data/spaces holds 157k real user images that the private raiw-app retention
checks materialize here. The only thing keeping them out of a commit was an
untracked .gitignore inside the directory, which a fresh clone does not
have.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Content Seal manifest named the production GPU worker in its provenance
column and in two row keys, several docstrings described their sources as user
uploads from the private corpus, and that corpus was called by the storage
product holding it. Each is now what a public reader can check: a pipeline and
strength, material that is not publication-cleared, and a private corpus. The
corpus test follows the renamed rows. Deployment and tier detail goes the same
way, since which backend each hosting tier can afford is a product decision,
not a library fact.
Twelve harnesses that need a cloud GPU account leave scripts/. The research
pages that cite their findings name them as harnesses kept outside this
repository and stop implying a reader can rerun them; the tracked inputs and
recorded verdicts remain the durable record, and the offline analysis step
stays here with its test. A development invariant states the rule so the next
harness is not added back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The rule used globs, which Claude Code ignores, so it loaded in every
session. CLAUDE.md keeps the fact that this gate does not pass
--ignore-unfixed and routes the lightning triage to docs/development.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Three withheld zero-face carriers, all confirmed zero-face with the same YuNet
detector the adaptive arms gate on, first cleared at 0.045 / 0.06 / 0.11. The
zero-face operating point is 0.11 + (0.11 - 0.045) = 0.175, which the shipped
0.20 already covers, so that floor now rests on both content classes rather than
only the face-bearing carriers it was derived from.
The content split this cohort kept suggesting does not survive the measurement:
the two classes differ by 0.025 while the spread WITHIN each is 0.065 and
0.0625, so a face split would key on a variable explaining about a quarter of
the variation it leaves behind.
The methodological point is the durable half. The fixture-only view of zero-face
OpenAI content spans 0.015 of boundary; the real carriers span 0.065, four times
wider. A narrow fixture spread yields a small worst-clean-plus-one-spread margin
and a false impression of homogeneity, and that impression is what made a split
look justified twice.
Every verdict is the public verifier's separate SynthID line with Content
Credentials reported not detected alongside, over RGB-identical metadata-stripped
positive controls.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ETI scoring program applies median blur and JPEG before decoding, so an
attack only wins if the watermark stays dead under ordinary handling. Our
removal paths are structural, so no driving question exists today; the note
records the hour-sized handling arm over removed-state artifacts as the
follow-up if removal durability under platform re-encoding ever becomes a
real question. Also records that participant attack code was never
published.
Verified against primary sources: the ETI Competition Data (NeurIPS 2024)
carries no license on the dataset card or in the evaluation repository and
stays off limits without organizer permission; the W-Bench dataset (ICLR
2025) is MIT while the VINE code is NTUITIVE non-commercial, so a profile
is buildable on our kernel without the VINE code. Also corrects two stale
internal notes: ETI attacked outputs are in the release, and W-Bench holds
10,000 instances, not 15k.
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.
The test monkeypatched huggingface_hub.snapshot_download by string, which
imports the real module. huggingface-hub rides the classify extra, not
the dev sync, so the CI matrix failed with ModuleNotFoundError on every
platform while the shared checkout venv hid the gap. Insert a stub into
sys.modules instead: the seam under test is classify's request, not the
hub client.
The clawhub job in distribute.yml compares the SKILL.md version with
the published one on every GitHub Release and submits an update when
they differ. Gated on the CLAWHUB_TOKEN secret: without it the job
skips with a notice, so the workflow lands safely before the secret
exists. A submitted update waits for ClawHub security scans, so a
green job means submitted, not listed.
The runtime's _weights_dir resolves the Hub snapshot offline through an
inline allow_patterns list, so a deployment that pre-caches the snapshot
for HF_HUB_OFFLINE=1 had to re-list the files by hand and silently
drifted when a release added one: the offline resolution then failed on
a four-fifths-present cache even though every missing piece had its own
fallback. Export the exact runtime request list as one public name, use
it in _weights_dir, and pin the seam with a test that captures the
snapshot_download call. Document the contract in photo-classify.md and
module-internals.md.
Issue #98. cv2's encoders write no container metadata, so every pixel-path
output (all, visible, invisible, erase) came back without its colour profile
and orientation tag, even with --keep-metadata.
imwrite now takes display_tags_from, the file whose decode produced the
pixels, and restores the two display tags into the re-encoded output: PNG and
JPEG by splicing iCCP/eXIf chunks and APP1/APP2 segments into the encoded
bytes, WebP by a lossless Pillow re-save (libwebp hides hand-spliced chunks
from readers without a VP8X header), HEIF through the save call itself.
Orientation is re-tagged only when the raster still has the source's stored
dimensions: cv2 rotates JPEG and (build-dependent) PNG decodes under
IMREAD_COLOR but never IMREAD_UNCHANGED, which is what the pixel paths read
with, so the decision is made against the raster's own shape and cannot
double-rotate.
The default metadata strip now preserves what the restore put back: the PIL
path forwards the profile and a scrubbed EXIF (orientation included) for PNG
and WebP outputs, the 16-bit chunk walker re-emits eXIf scrubbed instead of
dropping it wholesale, and --remove-all actually drops the ICC profile on
every format (Pillow's saver fallback kept it alive, contradicting the
documented "removed by --remove-all").
Mirror and 180-degree orientations (2, 3, 4) stay untagged: no cheap signal
distinguishes a rotated decode from a stored one for them.
ComposioHQ accepted a submission with no adoption bar (PR 1841);
VoltAgent lists reject brand-new skills, and the openclaw list indexes
ClawHub entries only. Both re-attempts are deferred with their
re-entry criteria.
Three invariants the deterministic suite did not pin: the version
triple (SKILL.md, plugin.json, marketplace.json), the 500-line SKILL.md
budget, and probe.py MIN_CLI_VERSION never exceeding the package
version. skill.yml gives skill-only changes a seconds-fast gate on top
of the full Tests workflow.
CLI-reachable through invisible --text-manifest, present since v0.27.0,
so no MIN_CLI_VERSION change. Skill version 1.0.2 across SKILL.md,
plugin.json, and marketplace.json. The receipt-abstention wording for
classify is deferred to the 0.38.0 release change, where it belongs with
the MIN_CLI_VERSION raise.
ffc46db5 carries receipt-gate-2026-09-02.npz and the receipt_gate
operating-point entry, so fresh classify installs load the gate from the
model snapshot; the package asset remains the fallback for pre-gate
weights directories.
The gate head is fitted on the freeze CLIP-L-ft embedding space, so it
belongs with the model: _load_receipt_gate now prefers
receipt-gate-2026-09-02.npz from the Hub snapshot or the
RAIW_CLASSIFY_WEIGHTS directory, with the package asset kept as the
fallback for weights frozen before the gate existed. Card mode of the
publish workflow now uploads the gate alongside the README and the
operating point, and the snapshot allow-pattern includes it.
Windows has no execute bit, so asserting one on the sandbox CLI copy failed both
Windows jobs while every other platform passed. The shim is launched through the
interpreter there anyway, so the assertion is POSIX-only.
The subprocess env is now overlaid on the real one rather than replacing it.
That was not the failure -- the hand-built env happened to work -- but a PATH of
`/usr/bin:/bin` and no SystemRoot is not something to leave in a test that CI
runs on Windows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The packaging tests a catalog runs say nothing about content, so the skill's
prose drifted from the CLI exactly the way an unguarded seam does. These tests
read the live Click tree: mark keys, flag choices, batch modes, pipeline
profiles, vendor cohorts, exit codes and extras must match what the skill says.
Guard the SET, not the instances. Per-flag comparisons only check what the skill
already documents, so `--vendor`, `--pipeline auto`, and then the whole
`classify` and `verify-openai-synthid` commands each shipped with a green suite.
`test_every_cli_command_is_named_somewhere_in_the_skill` closes that.
None of it proves the only thing a skill is for: that a model reaches for it and
then obeys it. `scripts/skill_eval.py` builds a throwaway project with the skill
installed and a recording stand-in for the CLI on PATH, runs a headless agent on
one user prompt, and grades the recorded command trace. Grading is mechanical --
"did it run `all` where the probe said nothing is written" is a fact in the
trace, and "did it name an output file that is not on disk" catches a fabricated
result without asking a model to judge itself. Agents are nondeterministic, so
the report is a pass rate per check over repeats, never one verdict.
The harness is tested too: the stand-in's exit codes are compared against the
real CLI where the state is reproducible locally, the grader runs over hand-built
traces, and every case is checked for internal consistency, because a scenario
whose `identify` names a mark its `visible` cannot find measures the case rather
than the skill.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`skills/remove-ai-watermarks/` teaches a coding agent to drive the CLI on
content the user generated, and refuses stock-agency and other third-party
paid-asset marks. It ships with this repository rather than as a fifth release
surface, and installs through `npx skills add` in any harness or as a Claude
plugin from `.claude-plugin/marketplace.json`.
`scripts/probe.py` measures capability rather than presence: a found binary said
nothing about whether the build can run a visible-mark command, so it writes a
blank PNG with zlib and struct and runs the installed `visible` and `invisible`
on it. Exit 2 is a state only a working stack reaches, and `invisible` checks
its extra before detection, so a signal-free image answers in 0.23s with no
model download. That distinction matters: without CUDA, `all` writes nothing
with the invisible extra installed and writes a useful visible+metadata result
without it, and both exit 1.
The skill also carries what a weak model gets wrong. It must report only what a
command in the session actually did, because a run on a machine with no pixel
stack produced a finished-looking removal report, complete with a size table for
a file nobody wrote. It must not read exit 2 as a verdict, because Click's usage
errors share that code. And it must get explicit consent before
`verify-openai-synthid`, which uploads the user's image to a third party.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The default package installs without `pixels`, and the Homebrew formula ships
exactly that build, so a first run that followed this project's own install
instructions died mid-command on a bare `ModuleNotFoundError: No module named
'cv2'` -- the one moment the user needed the extra named, and the only moment
they were not told it. `visible`, `erase`, `all` and `batch` now stop with the
install hint; `remove_batch` lets the same error out instead of counting one
absent package as an error per image, which turned a directory of 84 into 84
failures with no cause named.
Ask `optional_deps.pixels_available()` rather than matching the exception's
module name. An ImportError raised inside `cv2/__init__.py` carries `cv2.cv2`
and a numpy ABI mismatch carries `None`, so a name match let exactly the
tracebacks this guard exists for through.
`identify` keeps its historical no-op when the visible arm cannot decode, but
the report no longer stays silent about it: an install lacking the extra dropped
the visible Gemini mark from a marked image and read exactly like a clean scan.
The caveat names which silence happened, because an absent extra and an
unreadable file need different fixes.
`metadata --remove` reports surviving markers instead of crashing when the
raster-normalization recovery cannot import cv2, and the video runtime's install
hint is now shell-quoted like the others -- bare brackets are a glob in zsh.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The 2026-09-02 campaign measured the Aug ChatGPT renderer drift at the
Model 2 level and shipped nothing: leave-one-over-one recovers only the
margin near-miss, hard rejections resist sibling training, and every
refit variant regresses a sibling eval cell through the shared negative
pools (google -10 or meta -6). The drift corpus stays eval-only; the
tc260 producer split is deferred until ContentProducer metadata is
exported from the app database. Preconditions recorded for the next
retrain.
A DEFINITELY verdict now runs a linear receipt gate on the CLIP-L-ft
vector Model 1 already computes: a hit publishes unknown and skips the
124-d provider pass. Trained on CORD-v2 train (800, CC BY 4.0) plus 200
synthetic capture-style receipts against 2,400 ai_train negatives; the
threshold is certified on CORD validation and a synthetic holdout with
no field-receipt pixels. Field receipts false-AI 19/59 -> 2/59, CORD
89/99 -> 0/99, ai_test DEFINITELY cost 7/1,847.
The check read `latest_version` on the node object, which reports only the
ACTIVE version. The registry reviews an upload before activating it, so for the
length of that review a correctly published version looks exactly like a release
that was never synced -- and 0.37.0 hit precisely that: the node repo bumped to
0.1.37 requiring >=0.37.0, the registry accepted it as
NodeVersionStatusPending, and the check called it missing.
Read the versions list instead and branch on the status: active passes, pending
passes while saying so in the summary rather than claiming the node is live, and
anything else fails. A missing version still fails hard, which is the case this
check exists for. Sort the matches by createdAt rather than trusting the
endpoint's current newest-first ordering.
Verified against the live registry in all three states: 0.37.0 pending, 0.36.0
active, 0.99.0 absent and erroring.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two new commands since 0.36.0, so a minor bump: `classify` (photograph
AI-versus-camera from pixels, with a gated provider head, behind the new
`classify` extra) and `verify-openai-synthid` (the official remote verifier,
behind the new `verify` extra). Also measured auto engine selection and a
Doubao edge-mask artifact fix.
`classify` downloads its freeze from wiltodelta/raiw-photo-classify. That is
the first release to carry the Hub id at all, which is why the rename landed
before the bump rather than after.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>