Commit Graph
334 Commits
Author SHA1 Message Date
Victor Kuznetsov 43312b04eb Exclude Markdown from Ruff formatting 2026-07-25 17:10:47 -07:00
dependabot[bot]andGitHub bb9f9c959d chore(deps): bump the minor-and-patch group across 1 directory with 5 updates
Bumps the minor-and-patch group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [pillow-heif](https://github.com/bigcat88/pillow_heif) | `1.4.0` | `1.5.0` |
| [c2pa-python](https://github.com/contentauth/c2pa-python) | `0.36.0` | `0.37.1` |
| [transformers](https://github.com/huggingface/transformers) | `5.13.1` | `5.14.1` |
| [huggingface-hub](https://github.com/huggingface/huggingface_hub) | `1.23.0` | `1.24.0` |
| [ruff](https://github.com/astral-sh/ruff) | `0.15.21` | `0.16.0` |



Updates `pillow-heif` from 1.4.0 to 1.5.0
- [Release notes](https://github.com/bigcat88/pillow_heif/releases)
- [Changelog](https://github.com/bigcat88/pillow_heif/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bigcat88/pillow_heif/compare/v1.4.0...v1.5.0)

Updates `c2pa-python` from 0.36.0 to 0.37.1
- [Release notes](https://github.com/contentauth/c2pa-python/releases)
- [Changelog](https://github.com/contentauth/c2pa-python/blob/main/docs/release-notes.md)
- [Commits](https://github.com/contentauth/c2pa-python/compare/v0.36.0...v0.37.1)

Updates `transformers` from 5.13.1 to 5.14.1
- [Release notes](https://github.com/huggingface/transformers/releases)
- [Commits](https://github.com/huggingface/transformers/compare/v5.13.1...v5.14.1)

Updates `huggingface-hub` from 1.23.0 to 1.24.0
- [Release notes](https://github.com/huggingface/huggingface_hub/releases)
- [Commits](https://github.com/huggingface/huggingface_hub/compare/v1.23.0...v1.24.0)

Updates `ruff` from 0.15.21 to 0.16.0
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.21...0.16.0)

---
updated-dependencies:
- dependency-name: c2pa-python
  dependency-version: 0.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: huggingface-hub
  dependency-version: 1.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: pillow-heif
  dependency-version: 1.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: ruff
  dependency-version: 0.15.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: transformers
  dependency-version: 5.14.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-23 23:25:34 +00:00
Victor Kuznetsov 11afe68411 chore(release): v0.19.0 v0.19.0 2026-07-23 16:20:27 -07:00
Victor Kuznetsov c11181b9b5 Detect and attribute Bria AI, fal.ai, and Apple Photos Clean Up
Corpus-mined vendor gaps (42k-file metadata scan, 2026-07-23):
- Bria AI signs C2PA as "Bria Artificial Intelligence" with source type
  empty (no trainedAlgorithmicMedia), so identify was completely blind;
  registered with asserts_ai like Dreamina.
- fal.ai ("fal - Features & Labels Inc.", fal-ai/<model> generators)
  was detected via the source type but never attributed; registered,
  also asserts_ai as a pure generative platform.
- Apple Photos Clean Up (Apple Intelligence object removal) was detected
  as a generic made-with-AI tag; now attributed as an AI edit via the
  photoshop:Credit marker, and the credit value joins
  AI_GENERATOR_TOKENS so removal strips it in parity.
Each new test was verified red without its fix.
2026-07-23 16:15:48 -07:00
Victor Kuznetsov eb544806f6 Add AI-generation scorer trained on metadata-labeled corpus
scripts/ai_score.py trains a gradient-boosted classifier on labels
derived from the scan_dataset metadata (C2PA AI generators, TC260,
local pipelines vs camera/screenshot/editor output) and scores every
file from pixel and container statistics alone, so metadata-stripped
files still get a score. Temporal holdout on the production corpus:
AUC 0.966 / AP 0.988. CLIP ViT-L/14 was evaluated as an alternative
and lost on both accuracy (0.82) and cost, so the model uses the
structural features the scanner already collects; scoring is CPU-only
and I/O-bound.
2026-07-23 16:01:35 -07:00
Victor Kuznetsov dfe69772cd Polish the scanner after review: shared b64 helper, tri-state pixel mode
Route the pixel layer's base64 through the capped _b64 helper, replace
the pixels/pixels_full booleans with one pixel_mode parameter, record a
pixel skipped marker on oversized files, read EXIF from the already-read
bytes instead of re-reading the file, chunk the noise residual
convolution and replace the mgrid with 1D broadcast in the FFT features
(bit-identical values, ~300 MB less peak memory at 2048px), and fix the
raw-only docstring to account for the derived pixel layer.
2026-07-23 11:54:08 -07:00
Victor Kuznetsov 68fb3dded6 Fix numpy-free metadata mode after the pixel-layer merge
Guard module-level numpy touchpoints so the metadata-only mode imports
and runs without numpy installed (deferred annotations, lazy DCT basis);
pixel modes now exit early with a clear numpy requirement message.
2026-07-23 11:13:13 -07:00
Victor Kuznetsov 47ef60c2a1 Add optional pixel forensics layer to the dataset scanner
--pixels adds aggregate, non-reconstructable pixel statistics (block-DCT
histograms with Benford deviation, high-pass noise stats, FFT band
energies and CFA peaks, ELA stats, gradient and color histograms) plus
per-section timing for pipeline latency planning. --pixels-full adds the
privacy-lifting artifacts behind an explicit flag (perceptual hash,
128px thumbnail, coarse ELA/noise-residual/FFT-phase maps). Expensive
maps are computed once and shared between the scalar and artifact paths
(2.2x speedup vs the first split implementation). numpy is required only
for the pixel modes; the metadata-only mode stays numpy-free.
2026-07-23 11:06:45 -07:00
Victor Kuznetsov a981eb84b1 Make the dataset scanner raw-only and harden format sniffing
Strip all derived output: the library verdict, the XMP edit-trail
parse, and the IJG quality estimate. The script now collects raw bytes
plus mechanical container decodes only, is fully standalone (no
remove_ai_watermarks import), and resumes interrupted runs. Add GIF,
BMP, and TIFF magic bytes to sniff_format (GIF files in the wild were
reported as unknown). Validated on the full 42k-file corpus: 100%
coverage, zero error records.
2026-07-22 20:32:36 -07:00
Victor Kuznetsov df62800bf1 Add standalone dataset metadata/forensics scanner script
Single-file read-only scanner producing one JSONL record per image:
AI verdict via identify (metadata-only, no visible marks), full EXIF/
IPTC/XMP/PNG/JPEG/WebP/ISOBMFF inventories, C2PA store, JPEG encoder
forensics (quant tables, IJG quality, scan script, Huffman, subsampling),
XMP edit-trail fields, hashes/timestamps, macOS download provenance,
Live Photo pairing id. Runs standalone (pillow/piexif/c2pa-python) with
the library optional; supports gzip output, resume after interruption,
and head-only scanning of oversized files.
2026-07-22 17:53:02 -07:00
Victor Kuznetsov 80cdaff2d5 chore(release): v0.18.0 v0.18.0 2026-07-22 13:17:00 -07:00
Victor Kuznetsov f1a5eecf98 Register RunningHub, Baidu, and LibLibAI visible marks; park Qingyan and MiniMax (measured)
New engines, each calibrated on its TC260 USCC cohort and validated by a
full-corpus sweep (42009 files):
- runninghub: top-left corner (new corner="tl"), faint mid-gray text via
  the new raw-grayscale "gray" detection front-end, anchor-position gate
- baidu: text-run-only template (pill is a bright-blob magnet), load-bearing
  Doubao+Qwen rival margins, corner-extended footprint for the white tag
- liblib: bottom-center (new corner="bc"), Arial silhouette (font is the
  discriminative lever against latin UI text), logo-extended footprint

Qingyan parked (no clean-arm separation at any render/box), MiniMax/Hailuo
parked (1 visible frame, the xinghui rule); silhouettes kept as starting
points.
2026-07-22 13:03:03 -07:00
Victor Kuznetsov ba29eccc45 chore(release): v0.17.0 v0.17.0 2026-07-22 09:15:07 -07:00
Victor Kuznetsov 5d63b9161f Register the Kling 可灵AI 3.0 visible text mark; park Yuanbao and cat-logo (measured)
Kling (USCC cohort 91110108335469089C, n=30): kling_engine.py, gate 0.35
(clean p99 0.304 / max 0.320), strict-only, unimodal 0.12/short on the
shared ladder, fitted locate box, no rival margin (crossfire 1/400 doubao
below gate, 0 jimeng, 0 clean), parity 9/9 detect->fill->re-detect.
Suppresses the jimeng pill like doubao/qwen. identify gains visible_kling.

Yuanbao: measured negative -- the two-line italic block does not separate
from clean corners on either front-end at any render/box/font setting;
the fitted recipe stays in render_vendor_silhouettes.py MARK_OPTS.

cat-logo: cohort has only 2 unique carriers, parked on evidence; the
draw_catlogo silhouette already separates (0.50 vs clean max 0.333), so
registration is a gate pick once more uniques arrive.

vendor_mark_calibrate: --fit-geometry takes locate-box overrides (two-line
marks were clipped by the inherited box) and the aspect sweep reaches 0.62.
2026-07-22 08:49:06 -07:00
Victor Kuznetsov 3a659ededc chore(release): v0.16.0 v0.16.0 2026-07-21 18:35:04 -07:00
Victor Kuznetsov 008319c6a5 Register the Qwen 千问AI生成 visible text mark
Calibrated on the 117-frame TC260-producer cohort (vendor_cohort_harvest +
vendor_mark_calibrate, both committed here): per-mark 2-rung ladder
(0.78, 1.27) for the two measured size modes, fitted locate box (the mark
sits ~0.025 of the short side off the edge; doubao's box clipped the first
glyph), measured template aspect 0.26, gate 0.45 (clean p99 0.301).
Strict-only (the sub-gate band is non-Qwen banners), no rival margin
(0 cross-fires on 400 doubao / 298 jimeng / 286 clean frames).
83/83 real marks detector-clean after cv2 fill.

TextMarkConfig gains a per-mark ladder field; the shipped 3-rung default
is unchanged for every other mark.
2026-07-21 16:41:16 -07:00
Victor KuznetsovandClaude Opus 4.8 1d93704a4d Restructure the open items into a continuation list for the next session
The section had accumulated incremental edits: defects numbered out of order
(1, 2, 3a, 3, 5, 4), closed items mixed into the open table, and "what to do
next" spread across four subsections that partly repeated each other.

Now it opens with START HERE -- the prioritized next actions and the reason each
sits where it does -- followed by open defects only, renumbered 1-5. Closed items
move to their own subsection, keeping the faint-mask post-mortem because how it
escaped both parity and its own regression test is the instructive part.

Adds a table of the completed full runs with their artifacts and row counts, each
verified against the file on disk. That exists because the artifacts are the
answer to "did we actually cover X" -- relaunching a sweep costs hours and returns
nothing new, and this session nearly did exactly that before checking. It also
names the two fast confirmations (real_examples_e2e ~2 min, robustness_suite
~3 min) that a later change should run instead.

Adds invisible_engine.py:346 as an open defect: it still discards imwrite's
success flag, the same shape as the crash fixed in the previous commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 08:31:06 -07:00
Victor KuznetsovandClaude Opus 4.8 633fc3aa52 Report failed writes instead of crashing, and reject directories at parse time
The Tier E adversarial sweep (new, scripts/robustness_suite.py) drove the real CLI
over truncated, corrupt, zero-byte, absurdly-shaped and bomb inputs, unicode and
RTL paths, hostile output directories and concurrent runs. It found two crashes;
the /simplify review then reproduced a third and worse one.

1. A FAILED WRITE CRASHED ON THE SIZE REPORT. image_io.imwrite is contractually
   non-raising and returns False, but write_bgr_with_alpha discarded that bool and
   returned None, so no caller could tell a failed write from a successful one.
   Every write site then ran output.stat() to print the size, so a read-only
   destination died with a bare FileNotFoundError pointing at the stat rather than
   the write. The fix is deliberately NOT uniform: single-image commands exit via
   the new cli._write_output_or_exit; api._write_visible_result RAISES so a library
   caller gets an accurate error instead of a confusing FileNotFoundError from the
   downstream metadata strip; and the batch sites raise but never SystemExit,
   because the batch loop counts per-image exceptions and aborting would kill the
   whole run.

2. BATCH LOST DATA SILENTLY. Into a read-only output directory it wrote ZERO files
   for 2 inputs and exited 0 -- no traceback, no error, an empty output directory a
   wrapping service would read as a completed run. The robustness harness could not
   see this class at all, since it scored exit codes and traceback markers and this
   failure has neither; it now asserts on the artifacts written.

3. A DIRECTORY PASSED AS THE IMAGE crashed the metadata scanner with
   IsADirectoryError, because click.Path(exists=True) accepts directories. Fixed
   with dir_okay=False on all six source arguments, so argument parsing refuses it.

Also adds Tier B4 (scripts/resource_ceilings.py): peak RSS per fill backend from
1 MP to 25 MP, one fresh process per cell. migan 603->775 MB and lama 4679->4779 MB
are flat in input size, confirming the crop-around-the-mask design and both
documented figures; cv2 is the only backend that grows (74->440 MB, 5.9x). The
harness's own no-op check originally allocated a full-frame temp before reading
peak RSS and inflated the numbers with input size -- it now compares only the mask
box, and the conclusion survived re-measurement.

And scripts/real_examples_e2e.py, which drives every command over real corpus
examples and checks the outcome rather than the exit code: 6/6 provenance classes
identified, 10/10 metadata strips re-scan clean, all three fill backends write,
diffusion on MPS writes genuinely changed images. It records samsung as a real
partial (the faintest mark, 0.431 -> 0.404 against a 0.40 gate on the weakest of
its 3 corpus positives) and treats the gated pill's refusal to act as correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 22:45:02 -07:00
Victor KuznetsovandGitHub c2afb92832 Merge pull request #61 from wiltodelta/dependabot/uv/minor-and-patch-d57359b79f
chore(deps): bump the minor-and-patch group with 4 updates
2026-07-20 15:58:12 -07:00
Victor KuznetsovandClaude Opus 4.8 52eb40c2ca Mask faint text marks with the detector's match box, not a response threshold
The faint-mask fallback added for the tophat front-end thresholded the
max-normalized uint8 response at 0.5 -- which selects every non-zero pixel,
not "half the peak" as its comment claimed -- and filled ~120% of the corner
box on textured frames. Measured on 14 real faint-path frames (cv2 fill,
detector re-run after): the detector's own best-match box fills a 58.7%-median
corner box vs 120.9% for the threshold, both 100% detector-clean. Detection and
the mask now read one method, _tophat_best, whose score gates detection and
whose argmax box bounds the fill, so the two cannot drift by construction --
which is how the mismatch arose. The 0.5 constant is deleted.

Parity could not catch this (a mask that fills everything is trivially
detector-clean) and the regression test could not either: its flat fixture
gives every threshold the same box, so mutating the constant to 99.0 stayed
green. The fixture now carries texture and asserts the mask area is bounded,
not merely non-empty; it reproduces the corpus number (127% pre-fix).

Also lands the Tier B2 verification harnesses that found and bounded this:
- detector_response.py: response curves (detected AND maskable per cell); found
  the size response is a comb, contrast is near-irrelevant, no unmaskable cells.
- ladder_headroom.py: measured that a denser scale ladder recovers 7.6% of
  misses for a 2.52%->3.05% false-fire rise, and the one landscape rung that
  helps is a geometry shift that helps and hurts equally (1.7:1) -- do not add.
- cjk_tail_probe.py: a generic shared-tail (AI生成) template does not separate
  uncovered vendors from clean corners (0.407 vs clean p99 0.298).

Records the visible-parity re-run confirming the earlier front-end fix (doubao
91.8% -> 99.3%), and dedups the thrice-written stamp forward model into one
fill_quality.composite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:41:18 -07:00
Victor KuznetsovandClaude Opus 4.8 7d00debdca Record the full visible-parity run and the per-mark residual split
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:11:27 -07:00
Victor KuznetsovandClaude Opus 4.8 ac20cf3a3d Record open defects, unrun verification tiers and the stale torch triage
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:10:12 -07:00
Victor KuznetsovandClaude Opus 4.8 21d7e05014 Drop AGENTS.md, CLAUDE.md is the single source
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:02:09 -07:00
Victor KuznetsovandClaude Opus 4.8 cd16e8129d Resync AGENTS.md with CLAUDE.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 10:57:52 -07:00
Victor KuznetsovandClaude Opus 4.8 836d87ed68 Mask faint text marks the tophat front-end detects but binarization loses
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 10:57:08 -07:00
Victor KuznetsovandClaude Opus 4.8 c150180acf Add full-surface verification harnesses and corpus sweep plan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 08:14:50 -07:00
Victor KuznetsovandClaude Opus 4.8 cfefd9d819 Remove assume_ai, add tophat front-end and rival margin, fix two CLI defects
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 08:14:50 -07:00
Victor Kuznetsov a8f3536d3e Refactor watermark detection and provenance handling 2026-07-16 17:40:46 -07:00
Victor KuznetsovandClaude Opus 4.8 9618ac93c8 docs: record where the HF Space source lives and how it deploys
The Space's demo code (app.py etc.) lives in a SEPARATE private repo,
wiltodelta/raiw-hf-space, which nothing documented -- so finding it cost a long
detour through the Space's commit authorship and a hunt for a write token that
never existed locally. Record it, plus the deploy flow that replaced the old
web-UI editing: push to that repo's main -> sync-to-hf.yml mirrors the files via
HfApi.upload_folder (adds a commit on top of the Space history, never a force
push). Also call out the two automations that both touch the Space so they are
not confused: sync-to-hf.yml ships demo-code changes, while this repo's
distribute.yml factory-rebuilds the Space on a library release so its
`remove-ai-watermarks>=` pin re-resolves.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:24:46 -07:00
dependabot[bot]andGitHub 976935f5fe chore(deps): bump the minor-and-patch group with 4 updates
Bumps the minor-and-patch group with 4 updates: [torch](https://github.com/pytorch/pytorch), [transformers](https://github.com/huggingface/transformers), [huggingface-hub](https://github.com/huggingface/huggingface_hub) and [ruff](https://github.com/astral-sh/ruff).


Updates `torch` from 2.12.1 to 2.13.0
- [Release notes](https://github.com/pytorch/pytorch/releases)
- [Changelog](https://github.com/pytorch/pytorch/blob/main/RELEASE.md)
- [Commits](https://github.com/pytorch/pytorch/compare/v2.12.1...v2.13.0)

Updates `transformers` from 5.13.0 to 5.13.1
- [Release notes](https://github.com/huggingface/transformers/releases)
- [Commits](https://github.com/huggingface/transformers/compare/v5.13.0...v5.13.1)

Updates `huggingface-hub` from 1.22.0 to 1.23.0
- [Release notes](https://github.com/huggingface/huggingface_hub/releases)
- [Commits](https://github.com/huggingface/huggingface_hub/compare/v1.22.0...v1.23.0)

Updates `ruff` from 0.15.20 to 0.15.21
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.15.20...0.15.21)

---
updated-dependencies:
- dependency-name: torch
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: transformers
  dependency-version: 5.13.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
- dependency-name: huggingface-hub
  dependency-version: 1.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-and-patch
- dependency-name: ruff
  dependency-version: 0.15.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-15 08:46:35 +00:00
Victor KuznetsovandClaude Opus 4.8 71dae8fe25 chore(release): bump version to 0.15.3
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.15.3
2026-07-14 17:17:01 +03:00
Victor KuznetsovandClaude Opus 4.8 9fe12c3997 docs: note content-based strip routing + SDXL watermarker fixes
Document both fixes in CLAUDE.md (the metadata.py and watermark_remover.py
bullets). Also add a --backend flag to the visible-removal audit script so a
realistic quality pass can run the production MI-GAN fill instead of cv2
(removal SUCCESS is backend-independent, but only migan/lama reflect the
recovered-region quality a user actually gets).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:16:56 +03:00
Victor KuznetsovandClaude Opus 4.8 183793fbf2 fix(invisible): disable diffusers SDXL watermarker on removal output
The SDXL removal pipelines (sdxl + controlnet) were built without
add_watermarker=False, so diffusers embedded its default open "Stable
Diffusion XL" DWT-DCT invisible watermark on every output whenever
invisible-watermark is installed (the detect extra). A watermark REMOVER was
therefore replacing one detectable AI watermark (SynthID) with another: the
cleaned output re-read as AI (identify -> "Open invisible watermark: Stable
Diffusion XL"), observed on the SynthID validation sample.

Both SDXL loaders now call a shared _disable_sdxl_watermarker helper (mirrors
_maybe_add_fp16_vae; the ControlNetModel sub-model and the Qwen loader never
call it, since only the pipeline accepts the kwarg). Verified end to end: the
affected outputs re-run clean (is_ai=None, no open watermark).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:16:49 +03:00
Victor KuznetsovandClaude Opus 4.8 7f22852593 fix(metadata): route strip output format by content, not extension
remove_ai_metadata chose its save format (and the lossless-JPEG fast path)
from the OUTPUT file extension. On the ~2% of real uploads whose extension
lies about their content (a PNG served as .jpg is the common case, ~0.9% of
the corpus), the default flow -- which inherits the source's own extension --
re-encoded a lossless PNG/WebP into a real JPEG, silently degrading the pixels
and breaking the "work with originals" invariant.

Sniff the actual container from magic bytes (_sniff_image_format, reusing the
12-byte head already read for the ISOBMFF check) and route on content: a
misnamed lossless source (source-extension format != content) is preserved in
its true format, while a correctly-named source still honors a deliberate
output-extension conversion (source.png -> output.jpg). The JPEG-lossless gate
is likewise content-gated.

Found by a new metadata-removal parity audit over the local corpus
(scripts/metadata_removal_audit.py): 18170/18173 carriers strip cleanly, and
this fix takes the 208 pixel-integrity failures (all misnamed PNGs) to 0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 17:16:36 +03:00
Victor Kuznetsov d9204e2e1f chore(release): bump version to 0.15.2 v0.15.2 2026-07-13 12:27:06 +03:00
Victor KuznetsovandClaude Opus 4.8 2bdaa09e5f fix(metadata): strip bare AIGC in APP11 and AIGC in a standard PNG text chunk
Full-corpus strip audit surfaced 24 china_aigc survivors on real uploads:
- 19 JPEG carried the bare AIGC{...} blob in APP11 (0xEB). That marker's branch
  in _jpeg_app_carries_ai only tested for a C2PA/JUMBF manifest and RETURNED, so
  a bare AIGC there slipped past the generic AIGC check. The specific
  C2PA(APP11)/XMP(APP1)/IPTC(APP13) checks now fall through to the generic
  _is_aigc_exif_value drop, which runs for every APP marker they did not claim.
- PNG carried the {"AIGC":{...}} block in a STANDARD text chunk (Description).
  _is_ai_key keeps that key, so removal now also drops a text value carrying an
  AIGC block (_is_aigc_exif_value broadened to accept str; wired into the PNG
  re-save value filter), in parity with aigc_label's detection.

Verified on the corpus: decodable china_aigc survivors 27 -> 0; the 3 remaining
are truncated files the strip fail-safe (v0.15.1) copies through by design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 12:27:05 +03:00
Victor Kuznetsov 72f6734b45 chore(release): bump version to 0.15.1 v0.15.1 2026-07-13 11:54:53 +03:00
Victor KuznetsovandClaude Opus 4.8 220803c4d0 fix(metadata): remove_ai_metadata is fail-safe on a truncated/corrupt image
PIL raises OSError decoding a truncated file, which crashed remove_ai_metadata
(the PNG/WebP PIL re-save path) -- a direct library caller like a web worker
500s on a partial upload. ~0.2% of the real upload corpus is truncated. The
strip now probes decodability first and, on failure, copies the input through
unchanged and returns rather than raising (we cannot strip what we cannot parse),
mirroring strip_c2pa_boxes' fail-safe. identify already handled these.

The CLI `metadata --remove` on an unreadable file therefore now exits 0 with the
input passed through, not a clean error (exit 1) -- `visible`, which must decode
to remove a mark, still exits 1. Test updated to the per-command contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 11:54:33 +03:00
Victor Kuznetsov 1dc6fee91a chore(release): bump version to 0.15.0 v0.15.0 2026-07-13 10:50:20 +03:00
Victor KuznetsovandClaude Opus 4.8 a4c901ff39 fix: metadata-strip parity, input robustness, and detection/clash coverage
Bug fixes (each with a regression test):
- metadata strip parity across every marker placement: IPTC digitalSourceType
  in XMP, the Samsung post-EOI trailer, the China TC260 AIGC block in EXIF
  UserComment, a bare AIGC block in a non-standard APP segment, and the ISOBMFF
  EXIF path (AIGC + xAI) are all now stripped -- anything a scanner flags, the
  strip reaches
- Samsung genAIType detected when its trailer sits past the 512 KB scan window
  (file-tail read on large photos)
- crashes on edge inputs: Gemini detector on images with a short side < 16px,
  footprint_mask on a zero-size ndarray, the humanizer on chromatic_shift >=
  width, and the CLI on unreadable/corrupt/empty input (clean error, not a
  traceback)
- WebP written losslessly (cv2 quality 101), not lossy at 100
- the IPTC digitalSourceType algorithmicMedia (procedural, not trained on
  sampled data) is no longer flagged as AI-generated, so clean procedural
  content is not scrubbed
- c2pa source-type: compositeWithTrainedAlgorithmicMedia is checked before the
  bare algorithmicMedia token, so an AI-enhanced composite is not misclassified

Detection:
- integrity-clash coverage now normalizes ByteDance / Canva / ElevenLabs /
  Black Forest Labs, so a transplanted manifest next to an independent
  conflicting stamp is caught; the generic China TC260 AIGC label is attributed
  to a co-present TC260 vendor, so a legit Doubao image (its own C2PA + TC260
  label) does not clash (corpus-validated: 0 new clashes on 5000 carriers)

CLI:
- batch exits non-zero (with a warning) when any image errors or a GPU-missing
  SynthID scrub is skipped, and copies the input through so the output dir stays
  complete -- it used to always exit 0 and could silently drop files

Perf:
- GeminiEngine reused as a process-wide singleton with a precomputed template
  ladder: -24% on the identify sparkle path, detection byte-identical

Internal: one shared _ai_exif_targets rule set feeds both EXIF scrubbers so
their coverage cannot drift; docs synced; maintain.sh hardened so the uv-secure
internal teardown crash no longer aborts the gate (still fails on a real finding).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 10:49:24 +03:00
Victor KuznetsovandClaude Opus 4.8 190dc89d23 perf(visible): crop MI-GAN around the mask so peak RAM is bounded by mark size
erase_migan fed the whole frame to the ONNX model, so peak RSS scaled with the
upload (~0.6 GB at 4 MP up to ~2.4 GB at 25 MP). Mirror erase_lama: crop a padded
region around the mask (pad = max(256, 2*bbox)), feed only that crop (at native
resolution -- MI-GAN accepts arbitrary dims, unlike LaMa's fixed 512 square), and
paste only masked pixels back. Peak RSS is now bounded by the mark size
(~0.6-0.9 GB), so a memory-tight host (a 1-2 GB web worker) can run MI-GAN on a
25 MP upload.

Fill quality is unchanged: verified by eye on real Gemini/Doubao marks plus a
ground-truth reconstruction sweep -- a tighter view if anything reduces the GAN's
hallucination of large background structure.

Extract the shared padded-crop-box math into _padded_crop_box (used by both
erase_lama and erase_migan).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 08:22:59 +03:00
Victor KuznetsovandClaude Opus 4.8 f69fedcf2d chore(release): bump version to 0.14.1
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.14.1
2026-07-11 11:13:02 +03:00
Victor KuznetsovandClaude Opus 4.8 731af94639 docs(release): note HOMEBREW_TAP_TOKEN rotation on distribute.yml Bad credentials
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:13:02 +03:00
Victor KuznetsovandClaude Opus 4.8 49869ab02b feat(identify): detect Dreamina C2PA + Tencent Cloud AIGC schema
Two AI-provenance metadata types mined from the retained corpus that
identify previously read as no-signal:

- Dreamina (ByteDance's international Jimeng brand) signs C2PA as
  "Bytedance Pte. Ltd." with a "Dreamina/x.y" claim generator and NO
  digitalSourceType, so the generator name is the only AI signal. Add a
  C2paAiVendor row with a new asserts_ai flag (identity-AI: presence
  asserts AI without trainedAlgorithmicMedia) plus the derived
  C2PA_IDENTITY_AI_ORGS view, folded into identify's c2pa_is_ai. Keyed on
  the Dreamina generator token, not the "Bytedance Pte" issuer, so non-AI
  CapCut edits signed by the same entity stay unattributed. 7/7 corpus
  files now attribute to ByteDance.

- Tencent Cloud's TC260 AIGC variant uses a ServiceProvider/ServiceUser
  schema (vs the producer-side ContentProducer schema), embedded in EXIF
  ImageDescription; add those field names to _TC260_FIELDS so the generic
  {"AIGC":{...}} gate accepts it. 11/11 corpus files now flagged.

Test-first: reproducing tests in test_identify.py / test_metadata.py.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 10:54:56 +03:00
Victor KuznetsovandGitHub 41e20a0606 Merge pull request #60 from wiltodelta/claude/mystifying-cori-a4c1e7
v0.14.0: visible-mark localize→fill rewrite, backend priority, fixes
v0.14.0
2026-07-11 09:53:07 +03:00
Victor KuznetsovandClaude Opus 4.8 e3e332c4b1 Merge origin/main into claude/mystifying-cori-a4c1e7
main landed #58 (pill-gate fix, superseded by this branch's localize->fill
rewrite) and #57 (deps bump). Resolved the 6 code/test/doc conflicts by keeping
this branch's post-rewrite versions; the deps bump auto-merged into
uv.lock/pyproject. Full gate green after resolution: ruff, pyright 0, 730 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 09:50:21 +03:00
Victor KuznetsovandClaude Opus 4.8 b579385c6f feat(visible): white-core rescue for the Gemini false-positive gate
The FP gate demotes a low-gradient match, but a real FAINT sparkle also has soft
edges, so metadata-stripped faint sparkles were dropped. Keep a low-grad match
that is a strong (conf >= 0.52), bright, near-WHITE-core sparkle: a real sparkle
core is white, a clean bright corner that shape-matches (sky/sun) is colored
(_core_saturation). Recovers ~14/20 stripped faint sparkles under the DEFAULT
strict/auto (no metadata, no flag) at ~1.25% clean false-fire (baseline 0.55%);
the ~0.51-scoring bright-background FPs stay demoted (below 0.52).

A learned classifier on the same features measured WORSE than the tuned gate
(tier-1: MLP 86.7% recall vs the gate's 90.8% at equal false-fire), so the
heuristic stays; a patch-CNN with richer features is roadmapped P2 with low
expected value -- the precision/recall wall is fundamental (deep-research +
tier-1 both confirm it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 09:41:58 +03:00
Victor KuznetsovandClaude Opus 4.8 bd60be10f9 docs: fold visible-mark + detection-method research into watermarking-landscape
Deep-research 2026-07-10 (adversarially verified): the Gemini sparkle is
tier-gated (visible on Free/Pro, absent on Ultra/AI-Studio/API; no official
visible-mark detector or published glyph spec); the faint-visible-mark
precision/recall wall is fundamental (learned CNN front-end does not cleanly
separate true/false, arXiv:1705.08593 refuted); learned detectors need large
synthetic-composite datasets + carry off-distribution risk; landscape adds
Meta bottom-left + Samsung star-icon variants; China GB 45438-2025 is the
strongest visible-mark mandate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 10:00:12 +03:00
Victor KuznetsovandClaude Opus 4.8 27a921b54f docs(visible): record the 0.12.1-vs-0.14 head-to-head + backend quality
Full-dataset validation of reverse-alpha (v0.12.1) vs the current localize->fill:
doubao/jimeng identical (100% coverage + clearance across all backends); gemini
strict coverage a few points below reverse-alpha (the FP tightening), every
missed mark recovered under assume_ai, clearance ~98% both, no outside-box
damage. Clearance is fill-independent (cv2/MI-GAN/LaMa all strip the mark shape);
the difference is visual fill quality on textured/structured backgrounds -- LaMa
best, MI-GAN can ghost/hallucinate, cv2 smears -- which motivates auto = LaMa >
MI-GAN > cv2. Added to module-internals, known-limitations, and the CLAUDE.md
compact list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 19:18:25 +03:00
Victor KuznetsovandClaude Opus 4.8 c858006e93 feat(visible): auto fill prefers LaMa > MI-GAN > cv2, warn on cv2 fallback
The auto backend now resolves best-first: LaMa (highest quality, recovers the
textured/structured backgrounds the classical fill smears) > MI-GAN > cv2. Both
learned backends share the same onnxruntime availability check, so auto cannot
tell them apart and always prefers the better one; a memory-tight deployment
that cannot afford LaMa's ~4.7 GB peak pins MI-GAN explicitly via
`--backend migan` / `backend="migan"` (the deployment's call, not the library's).
cv2 stays the no-deps floor and now emits a one-time quality warning when auto
falls back to it, since it smears texture/structure.

Motivated by a v0.12.1 reverse-alpha vs 0.14 localize->fill head-to-head:
reverse-alpha recovered structured backgrounds more cleanly than any inpaint;
LaMa closes most of that gap, MI-GAN can ghost/hallucinate, cv2 is weakest.
doubao/jimeng removal is identical between versions; gemini strict coverage is
4pp lower (all recovered via assume_ai) with cleaner clearance and no
outside-box damage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 18:24:20 +03:00