Compare commits
No files matched your search
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Agent skill and Claude Code plugin for the remove-ai-watermarks CLI.",
|
||||
"version": "1.0.6"
|
||||
"version": "1.0.13"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -27,26 +27,35 @@ top-level Python API, and runtime detection/removal pipelines.
|
||||
|
||||
The unified maintainer entry point is `scripts/provider_oracles.py`; its workflow and
|
||||
slot schema live in `docs/provider-oracles.md`. Multi-account and multi-network work
|
||||
selects one named slot explicitly from the gitignored repository-local
|
||||
`.oracle-slots.json` by default and snapshots it into the immutable batch manifest.
|
||||
Never commit the local slot file.
|
||||
Do not add account or IP rotation, rate-limit failover, or cross-session result merging.
|
||||
selects each named slot explicitly from the gitignored repository-local
|
||||
`.oracle-slots.json` by default and snapshots it into a separate immutable batch
|
||||
manifest. A provider check may use the finite set of configured account slots and surfaces
|
||||
after a refusal. Do not add new logins, IP rotation, network-route failover,
|
||||
unbounded retries, or cross-session result merging. Never commit the local
|
||||
slot file.
|
||||
Every manual result stays bound to the exact sanitized upload hash and preserves the
|
||||
provider response verbatim.
|
||||
|
||||
Provider surface selection is API-first. OpenAI and Microsoft use their official API
|
||||
adapters before their Web surfaces when the requested media and local configuration are
|
||||
supported. Google and Meta currently use Web because this tooling has no usable API route
|
||||
for them. Web remains an explicit second surface, never an automatic retry after an API
|
||||
refusal, quota response, transport error, or indeterminate verdict.
|
||||
for them. A request to use a provider covers every configured surface, including Web,
|
||||
without a second confirmation. An API refusal, quota response, transport error, or
|
||||
indeterminate verdict stays on that surface and never silently submits the file to Web.
|
||||
|
||||
Google Web oracle automation uses only the user's existing authenticated real Chrome
|
||||
session and explicit `/u/N/` account index. OpenAI, Microsoft, and Meta Web automation
|
||||
session. Gemini uses an explicit `/u/N/` account index. The SynthID Detector at
|
||||
`https://synthid.com/` (`synthid-web`) uses the same Chrome profile and does not take a
|
||||
`/u/N/` index; its terms forbid driving the page as an automated API. OpenAI, Microsoft, and Meta Web automation
|
||||
uses isolated Playwright contexts; it must never attach to or copy state from the user's
|
||||
real browser. Proxy credentials are read only through a slot's named environment variable
|
||||
and never enter a manifest or log. Dotenv interpolation may derive routed proxy URLs from
|
||||
one secret key without mutating the process environment. A selected proxy context may
|
||||
accept that proxy's TLS interception certificate; direct contexts keep normal TLS checks.
|
||||
ThorData rotating endpoints are pinned to one manifest-derived sticky session for the
|
||||
life of a Web batch so one browser context does not change exit identity between page
|
||||
requests. This is route continuity, not account or IP failover; a refusal still ends on
|
||||
the selected slot and never selects another route automatically.
|
||||
Upload readiness is surface-specific: OpenAI reaches it at page load but keeps background
|
||||
requests alive, while Microsoft and Meta require network idle. Preserve exact provider
|
||||
errors as indeterminate evidence rather than retrying or interpreting them as clean.
|
||||
@@ -73,9 +82,12 @@ The same rule applies to install hints: name the extra that actually makes the c
|
||||
|
||||
Run `bash maintain.sh` from the repository root. The authoritative type gate is scoped to `src/`; full-project Pyright can exhaust Node memory on the ML dependency graph.
|
||||
|
||||
The full gate outlasts the agent's 10-minute foreground shell cap, which kills it mid-suite. Run it detached under `nohup caffeinate -dimu` with its exit code written to a file and poll that file. To stop a stray run, kill its own PID, never `pkill -f pytest`, which also takes down other sessions' suites on the machine.
|
||||
|
||||
The security scanner's exit status is authoritative, including a failure after a success
|
||||
message. `tests/test_maintenance.py` checks clean, vulnerable, and broken scanner
|
||||
runs through the real shell entry point. Recheck unresolved advisories using the
|
||||
runs through the real shell entry point, and pins the CI `security` job to the
|
||||
same scanner line with no `continue-on-error`. Recheck unresolved advisories using the
|
||||
procedure in `docs/development.md`; do not revive the old text-matching bypass.
|
||||
|
||||
Boundary modules for cv2, Torch, and Diffusers may carry narrow per-file relaxations for unknown third-party types. Keep pure-logic files strict, preserve the local piexif stub, and fix real errors before widening a pragma.
|
||||
@@ -129,6 +141,10 @@ Do not classify an entire module as untestable because its main path downloads a
|
||||
- the photo-classify gate in `test_classify.py`: DEFINITELY is ridge AND MLP,
|
||||
POSSIBLY does not emit a provider, and `identify` does not import
|
||||
`remove_ai_watermarks.classify`.
|
||||
- the source-export classifier in `test_source_classify.py`: the Hub revision
|
||||
and artifact hash are immutable, the pickle-free schema and exact GELU are
|
||||
validated, threshold mutation changes the real decision, and `identify`
|
||||
never consumes the result as watermark evidence.
|
||||
- the `InvisibleOptions` defaults, in `test_api.py`. When one signature promises to
|
||||
mirror another, compare them field by field rather than pinning the values you happen
|
||||
to know about, so the next field added on one side and not the other fails at the
|
||||
@@ -295,3 +311,7 @@ explicit unchanged decodes when comparing stored pixels. Execute Ubuntu Bash
|
||||
workflow harnesses only on POSIX hosts with Bash; keep their pure Python checks
|
||||
active on every platform. Round floating-point image output before uint8
|
||||
conversion when quantization must preserve constant values across OpenCV builds.
|
||||
OpenCV 5 draws Hershey `putText` glyphs smaller and heavier than 4.x, so a
|
||||
calibrated template drawn with `putText` ships as a committed PNG whose recipe
|
||||
refuses any OpenCV but 4.x (`scripts/render_video_hershey_templates.py`,
|
||||
`scripts/build_openart_alpha.py`); never render one at import time.
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"WebFetch",
|
||||
"WebSearch"
|
||||
]
|
||||
},
|
||||
"enabledPlugins": {
|
||||
"pyright-lsp@claude-plugins-official": true,
|
||||
"code-simplifier@claude-plugins-official": true,
|
||||
"claude-md-management@claude-plugins-official": true
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
name: provider-oracles
|
||||
description: Run hash-bound Google, OpenAI, Microsoft, or Meta watermark-oracle checks for this repository. Use for provider verification, multi-account Gemini checks, isolated Playwright checks, or recording oracle evidence.
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Provider oracles
|
||||
@@ -14,22 +16,39 @@ Named slots live in the gitignored repository-local
|
||||
pass `--slots PATH` only to override the local path.
|
||||
|
||||
Never substitute one provider's negative verdict for another provider's signal.
|
||||
Do not optimize candidates adaptively against an external oracle. A retry,
|
||||
different account, or different network is a new explicit operator decision,
|
||||
not automatic fallback.
|
||||
Do not optimize candidates adaptively against an external oracle.
|
||||
|
||||
## Account continuation
|
||||
|
||||
A request to verify a bounded set of files with a provider authorizes using that
|
||||
provider's configured account slots unless the user limits the account scope.
|
||||
Before starting, identify the finite set of available authenticated slots and
|
||||
prepare one immutable batch per slot. After a quota refusal or service failure,
|
||||
record the exact response, leave that slot's remaining rows unsubmitted, and
|
||||
continue automatically through the remaining authorized slots. Do not stop to
|
||||
request permission for each account or file already covered by the task.
|
||||
|
||||
Submit each frozen artifact at most once per slot in that pass. Preserve
|
||||
inconclusive results and disagreements between accounts. Stop when the bounded
|
||||
plan is complete or no authorized usable slot remains; report remaining rows.
|
||||
New logins, another provider, or a new network route require authorization.
|
||||
The oracles and slots already configured for the requested provider may be used
|
||||
again, including their Web surfaces, without a second confirmation. Account
|
||||
continuation does not authorize changing candidate pixels or erasing failures.
|
||||
|
||||
## Default surface priority
|
||||
|
||||
Prefer an official API adapter whenever that provider, media type, and configured
|
||||
slot support it. Use Web only when no usable API route exists or the operator
|
||||
explicitly requests the Web surface as a second check. For OpenAI and Microsoft,
|
||||
the default plans are API then Web. Google and Meta currently have only usable
|
||||
Web routes in this tooling. Inspect the order with `plan PROVIDER`.
|
||||
slot support it. For OpenAI and Microsoft, the default plans are API then Web.
|
||||
Google and Meta currently have only usable Web routes in this tooling. Google's
|
||||
routes are `gemini-web` and `synthid-web`. Inspect the order with `plan PROVIDER`.
|
||||
The same request may use every configured surface.
|
||||
|
||||
The priority is a selection policy, not automatic failover. An API refusal,
|
||||
quota response, transport error, or indeterminate verdict must be recorded and
|
||||
reported. Do not submit the same artifact to Web unless the operator separately
|
||||
authorizes that next surface.
|
||||
reported. The same request already covers every configured surface of that
|
||||
provider, including Web. Run Web as its own acknowledged check and keep its
|
||||
verdict separate. Do not replace the API result with the Web result.
|
||||
|
||||
## Route by surface
|
||||
|
||||
@@ -44,18 +63,51 @@ Playwright CLI runner, copied cookies, or a new login.
|
||||
real Chrome profile and navigate directly to
|
||||
`https://gemini.google.com/u/<google_account_index>/app`.
|
||||
3. Prepare an immutable `gemini-web` batch outside the repository.
|
||||
4. Before each upload, request the action-time confirmation required for sending
|
||||
that exact prepared file to Gemini. Do not upload the source path directly.
|
||||
5. Attach one prepared upload and ask: `Was this image/video/audio created or
|
||||
edited by Google AI?`
|
||||
For every image check, visually inspect the source first. Remove every visible
|
||||
AI-provider logo or label with the project's visible-mark remover, derive all
|
||||
paired controls and candidates from that cleaned image, and let `prepare` strip
|
||||
AI metadata. Inspect the prepared upload again and do not submit it if a visible
|
||||
provider mark remains. A model answer based on a visible logo is not a SynthID
|
||||
result.
|
||||
4. Confirm that the prepared files and provider are covered by the user's
|
||||
request or existing upload approval. Ask only if that scope is missing or
|
||||
changed; do not request confirmation again for each file or account in the
|
||||
authorized plan. Do not upload the source path directly.
|
||||
5. Attach one prepared upload and ask: `Does this image/video/audio contain a
|
||||
SynthID watermark? Use the SynthID verification tool only; do not infer from
|
||||
visible content, logos, text, or metadata.` Do not substitute a general
|
||||
Google-AI-origin question; it permits visual-content reasoning instead of a
|
||||
SynthID verdict.
|
||||
6. Read the settled SynthID tool outcome, not the surrounding model reasoning.
|
||||
Preserve unclear, quota, and refusal wording rather than interpreting it as
|
||||
clean.
|
||||
7. Record the verbatim response and verify the completed batch.
|
||||
|
||||
If the selected Chrome profile or `/u/N/` account is not already authenticated,
|
||||
stop and ask the user to select the intended existing session. Do not sign in or
|
||||
switch to another account implicitly.
|
||||
If a planned Chrome profile or `/u/N/` account is not already authenticated,
|
||||
record that it is unavailable and continue with another authorized existing
|
||||
session. Ask the user only when no usable authorized session remains. Do not
|
||||
start a new login.
|
||||
|
||||
### SynthID Detector
|
||||
|
||||
Use the user's real authenticated Chrome. Do not use isolated Playwright: the
|
||||
portal's additional terms forbid using the page as an automated API, bulk
|
||||
verification, and quota evasion.
|
||||
|
||||
1. Validate a `synthid-web` slot. It names `browser_profile` and `account_label`.
|
||||
It does not use `google_account_index` or a proxy variable.
|
||||
2. Prepare an immutable batch outside the repository. Inspect images the same
|
||||
way as for Gemini, and do not submit a file that still shows a provider logo.
|
||||
3. Open `https://synthid.com/` in the named Chrome profile. Accept the terms
|
||||
dialog when it is shown. Do not start a new login. If the page requires
|
||||
sign-in and that profile has no session, leave the row unsubmitted.
|
||||
4. Attach one prepared upload through the page's own file control.
|
||||
5. Read the settled label. `Watermark detected` is `detected`. `Watermark not
|
||||
detected` is `not_detected`. Quota, sign-in, and unclear wording stay
|
||||
`refused` or `indeterminate`.
|
||||
6. Record the verbatim response. The result covers partner SynthID from Google
|
||||
DeepMind, Nvidia, OpenAI, and Kakao, and it does not name which partner. Do
|
||||
not record it as a Gemini-only or OpenAI-only verdict.
|
||||
|
||||
### OpenAI, Microsoft, and Meta Web
|
||||
|
||||
@@ -88,13 +140,22 @@ OpenAI, Microsoft, and Meta Web slots may each name one of them through
|
||||
verify egress before relying on it, and never automatically change route after
|
||||
a refusal. Bound a batch before starting.
|
||||
|
||||
ThorData user-and-password endpoints without `sessid` rotate the exit on each
|
||||
request, which breaks a browser page whose resources must share one network
|
||||
identity. The runner therefore derives one sticky `sessid` from the immutable
|
||||
manifest hash and keeps it for that batch. A new prepared batch gets a new
|
||||
session; rerunning or resuming the same batch keeps its session while the proxy
|
||||
retains it. An explicit `sessid` in the configured route is preserved. This is
|
||||
session continuity, not permission to retry a refused batch on another route.
|
||||
|
||||
### OpenAI API
|
||||
|
||||
Use `check-openai` with one explicit upload acknowledgement. Keep named API keys
|
||||
as `OPENAI_API_KEY_1`, `OPENAI_API_KEY_2`, and `OPENAI_API_KEY_3` in the local `.env`, and select
|
||||
only their variable names through an `openai-api` slot's `api_key_env`. Never
|
||||
print a key, copy one into a slot or manifest, or fall back automatically to a
|
||||
second key. API slots use a direct SDK transport that ignores environment proxy
|
||||
print a key or copy one into a slot or manifest. Continue through authorized
|
||||
named API slots according to the bounded account plan above; keep a separate
|
||||
batch for each. API slots use a direct SDK transport that ignores environment proxy
|
||||
settings. The current adapter is image-only.
|
||||
|
||||
### Microsoft API
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
name: signal-discovery
|
||||
description: Discover missing AI provenance and watermark coverage in the remove-ai-watermarks library from fresh or historical corpora, then validate detection and removal parity. Use for corpus gap mining, periodic signal audits, new-provider discovery, or deciding whether old retained data must be replayed. Do not use for ordinary per-file identification or removal.
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Signal discovery
|
||||
|
||||
Work from the `remove-ai-watermarks` repository root. This skill discovers and
|
||||
closes library coverage gaps. Implementation, tests, fixtures, and
|
||||
documentation changes belong in this library; do not patch a downstream
|
||||
consumer to compensate for a detector or cleaner gap.
|
||||
|
||||
Treat retained inputs as sensitive and read-only. Keep reports, contact sheets,
|
||||
sidecars, and derived samples under the library's gitignored `.local-eval/`.
|
||||
Never commit a retained input or an artifact derived from one.
|
||||
Use only publication-cleared or synthetic files for tracked fixtures.
|
||||
|
||||
Before acting, read:
|
||||
|
||||
- `CLAUDE.md` and `.claude/rules/development.md`;
|
||||
- `docs/verification-plan.md`;
|
||||
- `docs/supported-signals.md` and `docs/watermarking-landscape.md`;
|
||||
- [references/coverage-and-cadence.md](references/coverage-and-cadence.md).
|
||||
|
||||
## Choose the run
|
||||
|
||||
Do not call `corpus_gap_scan.py` a complete signal audit. Select every mode the
|
||||
question requires:
|
||||
|
||||
1. **Fresh discovery** finds new providers, serializations, labels, and formats
|
||||
in objects added since the preceding review.
|
||||
2. **Historical replay** reruns current code on the complete retained history.
|
||||
Use it after detection-path or format-support changes and on the periodic
|
||||
backstop defined in the reference.
|
||||
3. **Removal parity** proves that every detected removable signal is cleared
|
||||
without violating the signal family's fidelity invariant.
|
||||
4. **Calibration or provider oracle** is required where the corpus has no
|
||||
self-evident answer key. Never treat a local negative as proof that a
|
||||
proprietary watermark is absent.
|
||||
|
||||
Start by inventorying the current code, not a remembered list. Enumerate image
|
||||
and video visible-mark registries, metadata readers and strippers, local
|
||||
invisible decoders, explicit invisible vendor routes, supported containers, and
|
||||
the stable signal families emitted by `identify`. Reconcile that inventory with
|
||||
the coverage matrix before interpreting any green result. A hand-written marker
|
||||
list or report family map that omits a current registry member is itself a
|
||||
finding.
|
||||
|
||||
## Run protocol
|
||||
|
||||
1. Record the library commit, version, enabled extras, corpus date range, and
|
||||
exact commands in an untracked run directory. Preserve the pre-run Git
|
||||
status so unrelated work is not attributed to the audit.
|
||||
2. Refresh the local retained corpus through its owning, external collection
|
||||
process when the task calls for fresh data. Use an explicit gitignored
|
||||
destination, do not delete local objects, and finish with a zero-delta pass.
|
||||
Fetch paired outputs only for a removal or complaint investigation.
|
||||
3. Run the applicable fresh and historical discovery modes from the reference.
|
||||
Use `corpus_gap_scan.py --since YYYY-MM-DD` for the overlapping fresh window
|
||||
and omit `--since` for the complete history. Report counts per input shape
|
||||
and per outcome; an example does not establish coverage. Give every code and
|
||||
corpus snapshot a distinct `--report` path. The adjacent
|
||||
`.progress.jsonl` checkpoint is the live progress source and makes the same
|
||||
command resumable; use `--restart` only when intentionally discarding it.
|
||||
4. Triage every candidate into one of: parser/serialization gap, missing vendor
|
||||
attribution, new visible mark, possible invisible signal, format/read error,
|
||||
expected non-AI provenance, or false candidate. Preserve unresolved items as
|
||||
unresolved.
|
||||
5. Reproduce an actionable gap with a failing test before changing code. Derive
|
||||
the smallest publication-safe fixture from a controlled source or construct
|
||||
it synthetically; do not move the retained file into a tracked path.
|
||||
6. Implement the fix in the library subsystem that owns it. Update the library
|
||||
registry, parser, remover, public docs, and published agent skill when their
|
||||
contracts move. Updating a downstream consumer to take a released version is
|
||||
a later, separately scoped task.
|
||||
7. Rerun the affected focused audit and the complete historical regression
|
||||
mode. A detection change must name the files expected to move before the
|
||||
after-run; classify lost signals separately from intended new detections.
|
||||
8. Run removal parity for every newly detected removable signal. A quiet
|
||||
post-removal detector is necessary but not always sufficient: check
|
||||
pixel/stream invariants and visually inspect residual-prone visible marks.
|
||||
9. Run `bash maintain.sh`. Report evidence limits, incomplete rows, crashes,
|
||||
unsupported formats, and oracle-unavailable cohorts explicitly.
|
||||
|
||||
External oracle uploads are separate mutations. Use the `provider-oracles`
|
||||
skill only for a bounded, already-selected candidate, and obtain the required
|
||||
action-time upload approval. Do not adapt candidates against an oracle, retry
|
||||
automatically, or substitute one provider's verdict for another's.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Coverage and cadence
|
||||
|
||||
Use this reference to choose the audit modes and decide whether current-only or
|
||||
historical data is sufficient.
|
||||
|
||||
## Coverage matrix
|
||||
|
||||
| Surface | Discovery evidence | Regression check | Removal proof |
|
||||
| --- | --- | --- | --- |
|
||||
| C2PA, EXIF, XMP, IPTC, PNG text, TC260, soft bindings, and container metadata | `scripts/corpus_gap_scan.py` plus review of unattributed and partially attributed reports | `scripts/sidecar_regression.py`; compare record-based and file-based identification when collection changes | `scripts/metadata_removal_audit.py`; signal absent after strip and decoded pixels/streams unchanged |
|
||||
| Registered visible image marks | `scripts/visible_positives.py` records strict and metadata-corroborated results, metadata cohorts, and blinded native-detail bands for uncovered cohorts plus an unbiased quiet sample | `scripts/visible_eval.py` exposes registry members without adjudicated scope, registered examples, and full-corpus crossfire | `scripts/visible_removal_audit.py`; same-mark redetection plus visual residual review and fill-quality checks |
|
||||
| Candidate China-AIGC visible marks | `scripts/vendor_cohort_harvest.py` partitions by producer identity without using pixels; inspect full-width top and bottom sheets | `scripts/vendor_mark_calibrate.py` against independently labelled positives, clean negatives, and neighbouring marks | Register only after the candidate gate survives full-corpus crossfire; then use the normal visible-removal audit |
|
||||
| Registered visible video marks | Review provider-labelled or controlled complete clips, not isolated frames alone | Full-clip tests and the local real-provider audit described in `docs/verification-plan.md` | Preserve sequence, timing, duration, and audio; second-pass detection must be quiet |
|
||||
| Locally decodable invisible image marks, including open DWT-DCT and TrustMark | Positive-control-gated corpus or constructed carriers; a negative on an uncontrolled carrier is inconclusive | `scripts/watermark_benchmark.py` and the relevant decoder tests with the required extras installed | Re-run the same decoder on output and retain a positive control in the same run |
|
||||
| Proprietary image marks: Google SynthID, Microsoft InvisMark, Meta Content Seal | Provider provenance can select a cohort but is not a local pixel decode; use bounded provider-labelled candidates | Hash-bound provider-oracle batches when a new route, profile, or claimed signal is evaluated | Source-positive and output-negative verdicts from that same provider; record C2PA separately from the pixel watermark |
|
||||
| Invisible video and audio marks | Matched provider or open-model cohorts with codec and degradation controls | Benchmark-kernel audio/video rows and temporal aggregation checks | Preserve the non-target stream; use the matching oracle and a control-positive run |
|
||||
| Explicit vendor regeneration with no detectable local signal | This is a user assertion or research cohort, not a discovered signal | Verify routing and profile selection separately from detection | Quality and provider-oracle evidence may certify a cohort, but must not create a local detection claim |
|
||||
|
||||
`classify` is a pixel-origin guess, not provenance, and must not be counted as a
|
||||
new signal. `erase --region` is user-directed editing, not automatic watermark
|
||||
detection, and is outside this audit.
|
||||
|
||||
## Gaps the legacy scanner cannot settle alone
|
||||
|
||||
`corpus_gap_scan.py` is a metadata serialization probe. It reports raw markers,
|
||||
input shapes, identify errors, integrity clashes, unattributed results, and
|
||||
blind-marker candidates, but it still cannot settle other useful classes, so
|
||||
inspect them separately:
|
||||
|
||||
- a known generic signal with missing or wrong vendor attribution;
|
||||
- a newly coexisting marker on a file that already has another AI verdict;
|
||||
- an entirely novel marker absent from the scanner's hand-written list;
|
||||
- visible image or video labels;
|
||||
- proprietary invisible marks without a local decoder;
|
||||
- detection/removal parity failures;
|
||||
- decoder crashes, unsupported extensions, and incomplete reads;
|
||||
- drift between stored identify sidecars and the current library;
|
||||
- crossfire where a new detector fires on an older neighbouring cohort.
|
||||
|
||||
Marker matching must remain metadata-region bounded to avoid random hits in
|
||||
compressed pixels, but its inventory should be reconciled with the current
|
||||
metadata constants and registry on every audit. Do not infer completeness from
|
||||
`No gap candidates` until the other rows of the matrix also ran.
|
||||
|
||||
For a visible discovery pass, use a fresh report path and pass `--sheets` to
|
||||
`visible_positives.py`. Review the numbered PNG files before opening
|
||||
`MANIFEST_DO_NOT_OPEN.csv`; metadata cohorts select where a mark may occur but do
|
||||
not label the pixels. The separate `unbiased-quiet` stratum is required because a
|
||||
new provider may carry neither a mapped producer code nor readable provenance.
|
||||
|
||||
## When to replay old data
|
||||
|
||||
Yes, old data must be rerun. Current code can learn to parse an old
|
||||
serialization, and a new detector can crossfire on an old provider cohort even
|
||||
when no new object demonstrates either problem. The canonical intervals,
|
||||
change triggers, and sampling rules live in
|
||||
[`docs/verification-plan.md#signal-discovery-cadence`](../../../../docs/verification-plan.md#signal-discovery-cadence).
|
||||
|
||||
Keep dated reports immutable enough to compare runs. Record the library SHA and
|
||||
version, corpus minimum and maximum dates, input counts per media/format shape,
|
||||
enabled extras, command line, and output paths. Never commit operational corpus
|
||||
counts or retained identifiers; summarize only durable conclusions in the
|
||||
canonical research document.
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "remove-ai-watermarks",
|
||||
"version": "1.0.13",
|
||||
"description": "Identify and remove AI provenance marks from images and video the user generated or edited, using the remove-ai-watermarks CLI.",
|
||||
"author": {
|
||||
"name": "wiltodelta",
|
||||
"url": "https://github.com/wiltodelta"
|
||||
},
|
||||
"homepage": "https://raiw.cc",
|
||||
"repository": "https://github.com/wiltodelta/remove-ai-watermarks",
|
||||
"license": "Apache-2.0",
|
||||
"keywords": [
|
||||
"ai-watermark",
|
||||
"watermark-removal",
|
||||
"c2pa",
|
||||
"synthid",
|
||||
"provenance",
|
||||
"gemini",
|
||||
"sora"
|
||||
],
|
||||
"skills": "./skills/",
|
||||
"interface": {
|
||||
"displayName": "Remove AI Watermarks",
|
||||
"shortDescription": "Identify and remove AI watermarks and provenance metadata",
|
||||
"longDescription": "Runs the open-source remove-ai-watermarks CLI locally to identify C2PA, IPTC, EXIF and TC260 provenance, visible AI-generator labels and supported invisible watermarks, then removes them from images and video the user generated or edited. It does not remove stock, agency or other third-party marks.",
|
||||
"developerName": "wiltodelta",
|
||||
"category": "Productivity",
|
||||
"capabilities": ["Read", "Write"],
|
||||
"websiteURL": "https://github.com/wiltodelta/remove-ai-watermarks",
|
||||
"defaultPrompt": [
|
||||
"Check this image for AI watermarks and provenance metadata",
|
||||
"Remove the AI metadata from my generated video without re-encoding it",
|
||||
"Remove the visible AI label from the corner of my image"
|
||||
],
|
||||
"brandColor": "#A78BFA",
|
||||
"composerIcon": "./assets/icon.svg",
|
||||
"logo": "./assets/icon.svg"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Local state and secrets never belong in a plugin bundle.
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.oracle-slots.json
|
||||
.local-eval/
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
# Generated media and model artifacts.
|
||||
data/
|
||||
*.mp4
|
||||
*.onnx
|
||||
*.safetensors
|
||||
*.pt
|
||||
@@ -6,6 +6,10 @@
|
||||
# Create and manage keys at: https://platform.openai.com/api-keys
|
||||
# OPENAI_API_KEY=
|
||||
|
||||
# GitHub token (optional; only for scripts/sync_c2pa_soft_bindings.py)
|
||||
# Raises the api.github.com rate limit from 60 to 5000 requests/hour. GH_TOKEN is read as a fallback.
|
||||
# GITHUB_TOKEN=
|
||||
|
||||
# Local photo-classifier weights (optional; otherwise fetched from Hugging Face)
|
||||
# Directory contents are documented in docs/photo-classify.md.
|
||||
# RAIW_CLASSIFY_WEIGHTS=/path/to/classifier-weights
|
||||
@@ -19,6 +23,11 @@
|
||||
# Fallback cache root when HF_HOME is unset (optional)
|
||||
# XDG_CACHE_HOME=/path/to/cache
|
||||
|
||||
# Gemini API key (optional; only for generating Google provider samples by hand)
|
||||
# Paid tier: Nano Banana 2 Lite and Gemini Omni Flash have no free tier.
|
||||
# Create and manage keys at: https://aistudio.google.com/apikey
|
||||
# GEMINI_API_KEY=
|
||||
|
||||
# Named OpenAI keys for provider-oracle slots (optional; keep each value secret)
|
||||
# OPENAI_API_KEY_1=
|
||||
# OPENAI_API_KEY_2=
|
||||
@@ -34,6 +43,7 @@
|
||||
|
||||
# ThorData residential proxies (optional; only for explicitly selected Web oracle routes)
|
||||
# Generate these URLs in the ThorData endpoint generator; do not commit credentials.
|
||||
# The Web runner pins a rotating user-and-password route to one sticky session per manifest.
|
||||
# THORDATA_ROUTE_US=http://td-customer-USERNAME-country-US:PASSWORD@HOST.pr.thordata.net:9999
|
||||
# THORDATA_ROUTE_DE=http://td-customer-USERNAME-country-DE:PASSWORD@HOST.pr.thordata.net:9999
|
||||
# THORDATA_ROUTE_GB=http://td-customer-USERNAME-country-GB:PASSWORD@HOST.pr.thordata.net:9999
|
||||
|
||||
@@ -14,18 +14,22 @@ updates:
|
||||
update-types:
|
||||
- "minor"
|
||||
- "patch"
|
||||
# The `all` extra must remain jointly resolvable. These candidates violate
|
||||
# compatibility constraints: older supported TrustMark builds require NumPy 1.x, OpenCV 4.12+
|
||||
# requires NumPy 2 on newer Python versions, and this project keeps tokenizers
|
||||
# below 0.23 for the stable Transformers path. Keep Dependabot on the
|
||||
# compatible lines until those constraints move.
|
||||
# The `all` extra must remain jointly resolvable. PaddleX 3.7 (the OCR
|
||||
# extra) requires NumPy <2.4, mirrored by `constraint-dependencies` in
|
||||
# pyproject.toml, and this project keeps tokenizers below 0.23 for the stable
|
||||
# Transformers path. Keep Dependabot on the compatible lines until those
|
||||
# constraints move. Transformers 5.16+ requires tokenizers 0.23.1+,
|
||||
# and Diffusers requires huggingface-hub <2. Mirror the manifest bounds.
|
||||
ignore:
|
||||
- dependency-name: "numpy"
|
||||
versions:
|
||||
- ">=2"
|
||||
- dependency-name: "opencv-python-headless"
|
||||
- ">=2.4"
|
||||
- dependency-name: "transformers"
|
||||
versions:
|
||||
- ">=4.12"
|
||||
- ">=5.16"
|
||||
- dependency-name: "huggingface-hub"
|
||||
versions:
|
||||
- ">=2"
|
||||
- dependency-name: "tokenizers"
|
||||
versions:
|
||||
- ">=0.23"
|
||||
|
||||
@@ -7,8 +7,8 @@ name: Distribute on release
|
||||
# - HF Space: factory-rebuild the version already pinned in the Space repo.
|
||||
# - ComfyUI: sync, test, and publish the node against the exact library release.
|
||||
# - ClawHub: publish the agent skill when its own version moved.
|
||||
# All wait for the freshly published sdist to appear on PyPI first, since the
|
||||
# Release event fires in parallel with publish.yml's upload.
|
||||
# All wait for the freshly published distribution to appear in both PyPI APIs
|
||||
# first, since the Release event fires in parallel with publish.yml's upload.
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -21,7 +21,12 @@ on:
|
||||
|
||||
jobs:
|
||||
resolve:
|
||||
# Model-only prereleases must not reach PyPI, Homebrew, Spaces, or ComfyUI.
|
||||
if: ${{ github.event_name != 'release' || (startsWith(github.event.release.tag_name, 'v') && !github.event.release.prerelease) }}
|
||||
runs-on: ubuntu-latest
|
||||
# Thirty attempts may spend up to 20 seconds each in the JSON request,
|
||||
# simple-index request, and inter-attempt delay. Leave bounded headroom.
|
||||
timeout-minutes: 25
|
||||
outputs:
|
||||
version: ${{ steps.v.outputs.version }}
|
||||
url: ${{ steps.sdist.outputs.url }}
|
||||
@@ -57,32 +62,41 @@ jobs:
|
||||
PYTHON
|
||||
|
||||
- name: Upload distributed release identity
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: distributed-release-${{ github.run_attempt }}
|
||||
path: release-identity.json
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Wait for the sdist on PyPI, capture url + sha256
|
||||
- name: Wait for the PyPI JSON and simple indexes, capture sdist metadata
|
||||
id: sdist
|
||||
run: |
|
||||
set -euo pipefail
|
||||
v="${{ steps.v.outputs.version }}"
|
||||
for i in $(seq 1 30); do
|
||||
if rel=$(curl -fsSL "https://pypi.org/pypi/remove-ai-watermarks/$v/json" 2>/dev/null); then
|
||||
url=$(echo "$rel" | python3 -c "import sys,json;d=json.load(sys.stdin);print(next((u['url'] for u in d['urls'] if u['packagetype']=='sdist'),''))")
|
||||
sha=$(echo "$rel" | python3 -c "import sys,json;d=json.load(sys.stdin);print(next((u['digests']['sha256'] for u in d['urls'] if u['packagetype']=='sdist'),''))")
|
||||
if rel=$(curl --connect-timeout 10 --max-time 20 -fsSL "https://pypi.org/pypi/remove-ai-watermarks/$v/json" 2>/dev/null); then
|
||||
read -r url sha <<< "$(echo "$rel" | python3 -c "import sys,json;u=next((u for u in json.load(sys.stdin)['urls'] if u['packagetype']=='sdist'),{});print(u.get('url',''),u.get('digests',{}).get('sha256',''))")"
|
||||
if [ -n "$url" ] && [ -n "$sha" ]; then
|
||||
echo "url=$url" >> "$GITHUB_OUTPUT"
|
||||
echo "sha=$sha" >> "$GITHUB_OUTPUT"
|
||||
echo "Found sdist for $v"
|
||||
exit 0
|
||||
# PyPI's JSON API can show a freshly uploaded file before the
|
||||
# simple index used by pip has caught up. Downstream release
|
||||
# jobs install by package version, so waiting on JSON alone
|
||||
# creates a release-time race for them.
|
||||
simple=$(curl --connect-timeout 10 --max-time 20 -fsSL https://pypi.org/simple/remove-ai-watermarks/ 2>/dev/null || true)
|
||||
artifact=${url##*/}
|
||||
if [[ "$simple" == *"$artifact"* ]]; then
|
||||
echo "url=$url" >> "$GITHUB_OUTPUT"
|
||||
echo "sha=$sha" >> "$GITHUB_OUTPUT"
|
||||
echo "PyPI JSON and simple indexes list $v"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo "sdist for $v not on PyPI yet (attempt $i), waiting..."
|
||||
sleep 20
|
||||
if [ "$i" -lt 30 ]; then
|
||||
echo "PyPI indexes do not both list $v yet (attempt $i), waiting..."
|
||||
sleep 20
|
||||
fi
|
||||
done
|
||||
echo "::error::sdist for $v did not appear on PyPI in time"
|
||||
echo "::error::PyPI JSON and simple indexes did not both list $v in time"
|
||||
exit 1
|
||||
|
||||
homebrew:
|
||||
@@ -90,7 +104,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the tap
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
repository: wiltodelta/homebrew-tap
|
||||
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
@@ -114,10 +128,91 @@ jobs:
|
||||
needs: resolve
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Factory-rebuild the HuggingFace Space
|
||||
- name: Bump the Space pin, then factory-rebuild
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RAIW_HF_SPACE_TOKEN }}
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
VERSION: ${{ needs.resolve.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
repo=wiltodelta/raiw-hf-space
|
||||
workflow=pin-library.yml
|
||||
|
||||
if [ -z "${GH_TOKEN:-}" ]; then
|
||||
echo "RAIW_HF_SPACE_TOKEN secret is not set; skipping the pin bump."
|
||||
else
|
||||
dispatched_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
gh workflow run "$workflow" \
|
||||
--repo "$repo" \
|
||||
--ref main \
|
||||
--raw-field library_version="$VERSION"
|
||||
|
||||
run_id=""
|
||||
for attempt in $(seq 1 30); do
|
||||
run_id=$(
|
||||
gh run list \
|
||||
--repo "$repo" \
|
||||
--workflow "$workflow" \
|
||||
--event workflow_dispatch \
|
||||
--limit 10 \
|
||||
--json databaseId,createdAt \
|
||||
| python3 -c "import json,sys; t=sys.argv[1]; print(next((r['databaseId'] for r in json.load(sys.stdin) if r['createdAt']>=t), ''))" \
|
||||
"$dispatched_at"
|
||||
)
|
||||
if [ -n "$run_id" ]; then
|
||||
break
|
||||
fi
|
||||
echo "Space pin run not visible yet (attempt $attempt), waiting..."
|
||||
sleep 2
|
||||
done
|
||||
if [ -z "$run_id" ]; then
|
||||
echo "::error::The dispatched Space pin run did not appear"
|
||||
exit 1
|
||||
fi
|
||||
for attempt in $(seq 1 180); do
|
||||
read -r status conclusion <<< "$(
|
||||
gh run view "$run_id" \
|
||||
--repo "$repo" \
|
||||
--json status,conclusion \
|
||||
--jq '[.status, (.conclusion // "")] | @tsv'
|
||||
)"
|
||||
if [ "$status" = "completed" ]; then
|
||||
if [ "$conclusion" = "success" ]; then
|
||||
echo "Space pin run $run_id succeeded"
|
||||
break
|
||||
fi
|
||||
echo "::error::Space pin run $run_id completed with $conclusion"
|
||||
exit 1
|
||||
fi
|
||||
echo "Space pin run $run_id is $status (attempt $attempt), waiting..."
|
||||
sleep 5
|
||||
done
|
||||
if [ "${status:-}" != "completed" ]; then
|
||||
echo "::error::Space pin run $run_id did not finish in time"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting for the Space requirements pin to show $VERSION..."
|
||||
pinned=""
|
||||
for attempt in $(seq 1 40); do
|
||||
reqs=$(curl -fsSL \
|
||||
"https://huggingface.co/spaces/wiltodelta/remove-ai-watermarks/raw/main/requirements.txt" \
|
||||
|| true)
|
||||
pinned=$(printf '%s\n' "$reqs" \
|
||||
| sed -n 's/^remove-ai-watermarks==\([0-9][0-9a-zA-Z.]*\).*/\1/p' \
|
||||
| head -1)
|
||||
echo " attempt $attempt: Space pin=$pinned"
|
||||
if [ "$pinned" = "$VERSION" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 15
|
||||
done
|
||||
if [ "$pinned" != "$VERSION" ]; then
|
||||
echo "::error::Space requirements.txt still pinned to ${pinned:-<none>}, not $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
python3 -m pip install --quiet huggingface_hub
|
||||
python3 - <<'PY'
|
||||
import os
|
||||
@@ -132,7 +227,7 @@ jobs:
|
||||
needs: resolve
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Publish the agent skill when its version moved
|
||||
env:
|
||||
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
|
||||
@@ -217,7 +312,12 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for attempt in $(seq 1 180); do
|
||||
# 0.40.0's node sync finished after this waiter: 180 * 5s is 15 min,
|
||||
# and the node suite plus registry upload ran longer. Keep the
|
||||
# workflow wait, then accept a registry row for this library version
|
||||
# so a late-but-published node is not reported as a missing release.
|
||||
version="${{ needs.resolve.outputs.version }}"
|
||||
for attempt in $(seq 1 360); do
|
||||
read -r status conclusion <<< "$(
|
||||
gh run view "$run_id" \
|
||||
--repo "$repo" \
|
||||
@@ -235,5 +335,39 @@ jobs:
|
||||
echo "ComfyUI sync run $run_id is $status (attempt $attempt), waiting..."
|
||||
sleep 5
|
||||
done
|
||||
echo "::error::ComfyUI sync run $run_id did not finish in time"
|
||||
echo "ComfyUI sync run $run_id did not finish in time; checking the registry."
|
||||
python3 -m pip install --quiet packaging
|
||||
if curl -fsSL "https://api.comfy.org/nodes/remove-ai-watermarks/versions" \
|
||||
| VERSION="$version" python3 -c '
|
||||
import json, os, sys
|
||||
from packaging.requirements import InvalidRequirement, Requirement
|
||||
from packaging.utils import canonicalize_name
|
||||
from packaging.version import Version
|
||||
version = os.environ["VERSION"]
|
||||
target = Version(version)
|
||||
def requires_release(value):
|
||||
try:
|
||||
requirement = Requirement(value)
|
||||
except InvalidRequirement:
|
||||
return False
|
||||
return (
|
||||
canonicalize_name(requirement.name) == "remove-ai-watermarks"
|
||||
and requirement.url is None
|
||||
and requirement.marker is None
|
||||
and target in requirement.specifier
|
||||
and any(s.operator == ">=" and Version(s.version) == target for s in requirement.specifier)
|
||||
)
|
||||
matches = [
|
||||
v for v in json.load(sys.stdin)
|
||||
if any(requires_release(d) for d in (v.get("dependencies") or []))
|
||||
]
|
||||
if not matches:
|
||||
sys.exit(1)
|
||||
newest = max(matches, key=lambda v: v.get("createdAt") or "")
|
||||
print("%s %s" % (newest.get("version"), newest.get("status")))
|
||||
'; then
|
||||
echo "Registry already lists a node for $version; the wait timed out after publish."
|
||||
exit 0
|
||||
fi
|
||||
echo "::error::ComfyUI sync run $run_id did not finish in time and the registry has no node for $version"
|
||||
exit 1
|
||||
@@ -0,0 +1,59 @@
|
||||
name: Publish image-source classifier to Hugging Face
|
||||
|
||||
# Manual only. The derivative weights live in a GitHub model-freeze release;
|
||||
# private training images and caches never enter this repository.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
confirm:
|
||||
description: "Type publish"
|
||||
required: true
|
||||
weights_tag:
|
||||
description: "GitHub Release tag holding openai-google-source-v1.npz"
|
||||
required: true
|
||||
default: image-source-freeze-2026-09-25-v1
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
if: ${{ github.event.inputs.confirm == 'publish' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Download derivative weights
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.event.inputs.weights_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p weights
|
||||
gh release download "$TAG" \
|
||||
--repo "${{ github.repository }}" \
|
||||
--pattern openai-google-source-v1.npz \
|
||||
--dir weights
|
||||
test -s weights/openai-google-source-v1.npz
|
||||
- name: Verify artifact hash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
expected="$(jq -r .artifact_sha256 docs/image-source-hf/metrics.json)"
|
||||
test "${#expected}" -eq 64
|
||||
echo "$expected weights/openai-google-source-v1.npz" | sha256sum --check
|
||||
- name: Publish to Hugging Face
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${HF_TOKEN:-}" ]; then
|
||||
echo "HF_TOKEN secret is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
uv run --no-project --with huggingface-hub \
|
||||
python scripts/publish_image_source_hf.py \
|
||||
--src weights \
|
||||
--message "Image-source classifier freeze (GitHub ${SHA:0:7})"
|
||||
@@ -0,0 +1,142 @@
|
||||
name: Publish to MCP registry
|
||||
|
||||
# Lists the released PyPI package in the official MCP registry
|
||||
# (registry.modelcontextprotocol.io) from server.json at the release tag.
|
||||
# GitHub OIDC proves ownership of the io.github.wiltodelta namespace, so no
|
||||
# interactive login or stored secret is involved. The registry validates the
|
||||
# PyPI release itself (the mcp-name marker in its description), and the Release
|
||||
# event fires in parallel with publish.yml's upload, so this waits until PyPI
|
||||
# serves the version first. A version the registry already lists is skipped.
|
||||
# It is a workflow of its own, not a distribute.yml job, so a manual dispatch
|
||||
# re-lists one version without re-running Homebrew, the Space, or ComfyUI, and
|
||||
# a registry outage never fails release verification.
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Released version to list (no leading v), e.g. 0.46.0"
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
# Model-only prereleases are not Python package releases.
|
||||
if: ${{ github.event_name != 'release' || (startsWith(github.event.release.tag_name, 'v') && !github.event.release.prerelease) }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
SERVER_NAME: io.github.wiltodelta/remove-ai-watermarks
|
||||
# The same name percent-encoded, as the registry's path segment takes it.
|
||||
SERVER_PATH: https://registry.modelcontextprotocol.io/v0/servers/io.github.wiltodelta%2Fremove-ai-watermarks
|
||||
# Pinned publisher release; the digest is GitHub's sha256 for the asset.
|
||||
PUBLISHER_URL: https://github.com/modelcontextprotocol/registry/releases/download/v1.8.1/mcp-publisher_linux_amd64.tar.gz
|
||||
PUBLISHER_SHA256: a06c9096dcb9727c13555b6be26c7effa707b01f06a4c561ba7a3635443cf2cc
|
||||
|
||||
steps:
|
||||
- name: Resolve target version
|
||||
id: v
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
INPUT_VERSION: ${{ github.event.inputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "$EVENT_NAME" = "release" ]; then
|
||||
v="${RELEASE_TAG#v}"
|
||||
else
|
||||
v="$INPUT_VERSION"
|
||||
fi
|
||||
# The version becomes a git ref and a URL component. Reject malformed input.
|
||||
[[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+([a-zA-Z0-9.+-]*)$ ]]
|
||||
echo "version=$v" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Skip a version the registry already lists
|
||||
id: listed
|
||||
env:
|
||||
VERSION: ${{ steps.v.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
status=$(curl -sS --connect-timeout 10 --max-time 20 -o /dev/null -w '%{http_code}' "$SERVER_PATH/versions/$VERSION")
|
||||
case "$status" in
|
||||
200) echo "listed=true" >> "$GITHUB_OUTPUT"
|
||||
echo "- MCP registry: $SERVER_NAME $VERSION was already listed" >> "$GITHUB_STEP_SUMMARY" ;;
|
||||
404) echo "listed=false" >> "$GITHUB_OUTPUT" ;;
|
||||
*) echo "::error::Registry lookup returned HTTP $status"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# mcp-publisher reads only ./server.json, so fetch nothing else.
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
if: ${{ steps.listed.outputs.listed == 'false' }}
|
||||
with:
|
||||
ref: v${{ steps.v.outputs.version }}
|
||||
sparse-checkout: server.json
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Check server.json matches the release
|
||||
if: ${{ steps.listed.outputs.listed == 'false' }}
|
||||
env:
|
||||
VERSION: ${{ steps.v.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PYTHON'
|
||||
import json, os
|
||||
manifest = json.load(open("server.json", encoding="utf-8"))
|
||||
version = os.environ["VERSION"]
|
||||
assert manifest["name"] == os.environ["SERVER_NAME"], manifest["name"]
|
||||
found = [manifest["version"], *(p["version"] for p in manifest["packages"])]
|
||||
assert all(v == version for v in found), f"server.json versions {found} != {version}"
|
||||
PYTHON
|
||||
|
||||
- name: Wait for PyPI to serve the release
|
||||
if: ${{ steps.listed.outputs.listed == 'false' }}
|
||||
env:
|
||||
VERSION: ${{ steps.v.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in $(seq 1 40); do
|
||||
status=$(curl -sS --connect-timeout 10 --max-time 20 -o /dev/null -w '%{http_code}' \
|
||||
"https://pypi.org/pypi/remove-ai-watermarks/$VERSION/json" || true)
|
||||
if [ "$status" = "200" ]; then
|
||||
echo "PyPI serves $VERSION"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $attempt: PyPI returned HTTP $status for $VERSION"
|
||||
sleep 30
|
||||
done
|
||||
echo "::error::PyPI never served $VERSION"
|
||||
exit 1
|
||||
|
||||
- name: Publish with a verified mcp-publisher
|
||||
if: ${{ steps.listed.outputs.listed == 'false' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -fsSL -o mcp-publisher.tar.gz "$PUBLISHER_URL"
|
||||
echo "$PUBLISHER_SHA256 mcp-publisher.tar.gz" | sha256sum -c -
|
||||
tar xzf mcp-publisher.tar.gz mcp-publisher
|
||||
./mcp-publisher login github-oidc
|
||||
./mcp-publisher publish
|
||||
|
||||
- name: Read the entry back
|
||||
if: ${{ steps.listed.outputs.listed == 'false' }}
|
||||
env:
|
||||
VERSION: ${{ steps.v.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for attempt in $(seq 1 10); do
|
||||
status=$(curl -sS --connect-timeout 10 --max-time 20 -o /dev/null -w '%{http_code}' \
|
||||
"$SERVER_PATH/versions/$VERSION" || true)
|
||||
if [ "$status" = "200" ]; then
|
||||
echo "- MCP registry: $SERVER_NAME $VERSION is listed" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $attempt: registry returned HTTP $status"
|
||||
sleep 15
|
||||
done
|
||||
echo "::error::The registry does not list $SERVER_NAME $VERSION"
|
||||
exit 1
|
||||
@@ -6,8 +6,9 @@ name: Publish photo classifier to Hugging Face
|
||||
# Hub model: wiltodelta/raiw-photo-classify
|
||||
# Secret: HF_TOKEN, write-role Hugging Face token (same secret Space sync uses).
|
||||
#
|
||||
# Weights are not in git. Full mode downloads them from a GitHub Release
|
||||
# of this repository, then uploads with the card from docs/photo-classify-hf/.
|
||||
# Weights are not in git. Full mode downloads the frozen PyTorch files from a
|
||||
# GitHub Release, exports the vision-only ONNX graph, then uploads everything
|
||||
# with the card from docs/photo-classify-hf/.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -35,9 +36,9 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
@@ -52,6 +53,14 @@ jobs:
|
||||
gh release download "$TAG" --repo "${{ github.repository }}" --dir weights
|
||||
ls -l weights
|
||||
|
||||
- name: Export the vision-only ONNX graph
|
||||
if: ${{ github.event.inputs.mode == 'full' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
uv run --extra classify --with onnx python scripts/export_photo_classify_onnx.py \
|
||||
--checkpoint weights/clip-l-ft.pt \
|
||||
--out weights/clip-l-ft-vision-fp32.onnx
|
||||
|
||||
- name: Publish to Hugging Face
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
name: Publish source classifier to Hugging Face
|
||||
|
||||
# Manual only. The derivative model weights live in a GitHub model-freeze
|
||||
# release; the private training corpus and caches never enter this repository.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
confirm:
|
||||
description: "Type publish"
|
||||
required: true
|
||||
weights_tag:
|
||||
description: "GitHub Release tag holding source-pipeline-mlp.npz"
|
||||
required: true
|
||||
default: source-classify-freeze-2026-09-16-r2
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
if: ${{ github.event.inputs.confirm == 'publish' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Download derivative model weights
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ github.event.inputs.weights_tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p weights
|
||||
gh release download "$TAG" \
|
||||
--repo "${{ github.repository }}" \
|
||||
--pattern source-pipeline-mlp.npz \
|
||||
--dir weights
|
||||
test -s weights/source-pipeline-mlp.npz
|
||||
|
||||
- name: Verify the frozen artifact
|
||||
run: |
|
||||
set -euo pipefail
|
||||
expected="$(jq -r .artifact_sha256 docs/source-classify-hf/metrics.json)"
|
||||
test "${#expected}" -eq 64
|
||||
echo "$expected weights/source-pipeline-mlp.npz" | sha256sum --check
|
||||
|
||||
- name: Publish to Hugging Face
|
||||
env:
|
||||
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||
SHA: ${{ github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${HF_TOKEN:-}" ]; then
|
||||
echo "HF_TOKEN secret is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
uv run --no-project --with huggingface-hub \
|
||||
python scripts/publish_source_classify_hf.py \
|
||||
--src weights \
|
||||
--message "Source-classify freeze (GitHub ${SHA:0:7})"
|
||||
@@ -10,12 +10,14 @@ permissions:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
# Model-only prereleases are not Python package releases.
|
||||
if: ${{ startsWith(github.event.release.tag_name, 'v') && !github.event.release.prerelease }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: pypi
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
|
||||
- name: Verify release tag matches package version
|
||||
run: |
|
||||
@@ -38,4 +40,4 @@ jobs:
|
||||
# is needed. uv's uploader also accepts Metadata-Version 2.5 -- the permanent
|
||||
# fix that lets the hatchling pin be relaxed (see pyproject [build-system]).
|
||||
- name: Publish to PyPI
|
||||
run: uv publish
|
||||
run: uv publish
|
||||
@@ -32,8 +32,8 @@ jobs:
|
||||
skill:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
- name: Sync dev environment
|
||||
run: uv sync --frozen --extra dev
|
||||
- name: Agent skill packaging tests
|
||||
|
||||
@@ -16,17 +16,32 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
- name: Sync dev environment
|
||||
run: uv sync --frozen --extra dev
|
||||
- name: Check C2PA soft-binding registry snapshot
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: uv run python scripts/sync_c2pa_soft_bindings.py --check
|
||||
- name: Ruff lint
|
||||
run: uv run ruff check
|
||||
- name: Ruff format check
|
||||
run: uv run ruff format --check
|
||||
|
||||
security:
|
||||
name: lockfile vulnerability scan
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
with:
|
||||
enable-cache: false
|
||||
# Policy and triaged blocks: docs/development.md "Known security-gate blocks".
|
||||
- name: Scan uv.lock with uv-secure
|
||||
run: uvx uv-secure uv.lock
|
||||
|
||||
test:
|
||||
name: test ${{ matrix.os }} py${{ matrix.python-version }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -54,8 +69,8 @@ jobs:
|
||||
- os: windows-latest
|
||||
python-version: "3.14"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Sync dev environment
|
||||
@@ -67,8 +82,8 @@ jobs:
|
||||
name: versioned transport contracts py3.14
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
with:
|
||||
python-version: "3.14"
|
||||
- name: Create clean runtime contract environment
|
||||
@@ -89,8 +104,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: astral-sh/setup-uv@v7
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Install ffmpeg
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
steps:
|
||||
- name: Download distributed release identity
|
||||
if: ${{ github.event_name == 'workflow_run' }}
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
pattern: distributed-release-*
|
||||
path: distribution-identities
|
||||
|
||||
@@ -6,8 +6,10 @@ You are a **principal Python engineer** maintaining a CLI tool and library for r
|
||||
|
||||
The project gives users control over provenance marks on content they generated or edited themselves. It does not automatically remove stock-agency, marketplace, classifieds, tiled-preview, or other marks that protect a third party's paid or copyrighted asset.
|
||||
|
||||
- Add visible templates only for AI-generation labels.
|
||||
- Add visible templates only for marks the vendor declares as AI-generation labels, whatever plan hides them; plan-removable product branding ("Made with Gamma") is not one.
|
||||
- Do not add stock, agency, or classifieds marks to `watermark_registry.py`.
|
||||
- Do not add templates for regulator-defined disclosure icons (EU Code of Practice Annex 1).
|
||||
- LLM text watermarks are out of scope.
|
||||
- Keep `erase --region` generic and user-directed; do not build an automatic stock-watermark remover on it.
|
||||
|
||||
Full boundary and legal context: [`docs/legal-and-safety.md`](docs/legal-and-safety.md).
|
||||
@@ -31,19 +33,21 @@ Optional features and installation groups are documented in [`docs/installation.
|
||||
|
||||
`maintain.sh` runs dependency freshness and security checks, Ruff, Pyright scoped to `src/`, and the parallel test suite. Full-project Pyright is not the project gate because the ML dependency graph can exhaust Node memory.
|
||||
|
||||
The gate does not pass `--ignore-unfixed` to uv-secure, so a transitive CVE with no released fix stops it before Ruff, Pyright and the tests run, which a sibling repository that ignores unfixed findings would not do. The global rule applies: run and report those three separately. Scanner failures are fatal too. Triaged blockers and the recheck procedure: `Known security-gate blocks` in [`docs/development.md`](docs/development.md).
|
||||
The gate does not pass `--ignore-unfixed` to uv-secure, so a transitive CVE with no released fix stops it before Ruff, Pyright and the tests run. This matches the global rule: run and report those three separately. Scanner failures are fatal too. Triaged blockers and the recheck procedure: `Known security-gate blocks` in [`docs/development.md`](docs/development.md).
|
||||
|
||||
Command, gate, typing, and model-test invariants auto-load from [`.claude/rules/development.md`](.claude/rules/development.md). Environment recovery, CI behavior, and fixture policy live in [`docs/development.md`](docs/development.md).
|
||||
|
||||
Before a release, read [`docs/release-and-distribution.md`](docs/release-and-distribution.md). Treat the release as complete only after PyPI, Homebrew, the Hugging Face Space, and the ComfyUI Registry are verified; conda is not published. Keep the source-distribution public allowlist.
|
||||
Before a release, read [`docs/release-and-distribution.md`](docs/release-and-distribution.md). Treat the release as complete only after PyPI, Homebrew, the Hugging Face Space, and the ComfyUI Registry are verified; conda is not published. The MCP registry listing publishes itself from `publish-mcp-registry.yml` and does not block a release. Keep the source-distribution public allowlist.
|
||||
|
||||
The published Agent Skill is [`skills/remove-ai-watermarks/`](skills/remove-ai-watermarks/). Update it in the same change as any CLI edit that alters commands, extras, exit codes, mark keys, or the intended-use boundary. Install and publish steps: [`docs/agent-skill.md`](docs/agent-skill.md).
|
||||
|
||||
The same skill tree is the Codex plugin (`.codex-plugin/plugin.json`). `remove-ai-watermarks mcp` is a standard-library MCP server that runs one CLI command per tool call; keep it dependency-free, because the MCP registry launches it as a bare `uvx remove-ai-watermarks mcp` that cannot add extras ([`docs/mcp-server.md`](docs/mcp-server.md)).
|
||||
|
||||
## Module architecture
|
||||
|
||||
[`docs/module-internals.md`](docs/module-internals.md) is the canonical per-module map, including design decisions, thresholds, calibration history, incident records, and regression guards. Read the relevant section before changing a subsystem.
|
||||
|
||||
Research and current constraints are routed through [`docs/index.md`](docs/index.md), especially [`docs/known-limitations.md`](docs/known-limitations.md), [`docs/supported-signals.md`](docs/supported-signals.md), [`docs/synthid.md`](docs/synthid.md), and [`docs/watermarking-landscape.md`](docs/watermarking-landscape.md). Pixel photo classification is [`docs/photo-classify.md`](docs/photo-classify.md); it is not `identify`. Classifier research is split between general [`AI-generated image classifiers`](docs/ai-generated-image-classifiers.md) and [`SynthID source classifiers`](docs/synthid-classifiers.md). Other SynthID campaign logs are [`docs/synthid-detector-research.md`](docs/synthid-detector-research.md) and [`docs/synthid-removal-research.md`](docs/synthid-removal-research.md). The pre-split chronological archive is [`docs/synthid-detector-removal-plan.md`](docs/synthid-detector-removal-plan.md). The benchmark kernel and its pinned local oracles (AudioSeal, VideoSeal) cover the audio/video watermark cohorts and research studies: [`docs/benchmark-kernel.md`](docs/benchmark-kernel.md).
|
||||
Research and current constraints are routed through [`docs/index.md`](docs/index.md), especially [`docs/known-limitations.md`](docs/known-limitations.md), [`docs/supported-signals.md`](docs/supported-signals.md), [`docs/synthid.md`](docs/synthid.md), and [`docs/watermarking-landscape.md`](docs/watermarking-landscape.md). Pixel photo classification is [`docs/photo-classify.md`](docs/photo-classify.md); the separate OpenAI/Google/unknown export-pipeline signal is [`docs/source-classify.md`](docs/source-classify.md). Neither runs inside `identify` or constitutes SynthID detection. Classifier research is split between general [`AI-generated image classifiers`](docs/ai-generated-image-classifiers.md) and [`SynthID source classifiers`](docs/synthid-classifiers.md). Other SynthID campaign logs are [`docs/synthid-detector-research.md`](docs/synthid-detector-research.md) and [`docs/synthid-removal-research.md`](docs/synthid-removal-research.md). The pre-split chronological archive is [`docs/synthid-detector-removal-plan.md`](docs/synthid-detector-removal-plan.md). The benchmark kernel and its pinned local oracles (AudioSeal, Perth, PixelSeal, VideoSeal) cover image, audio, and video watermark cohorts and research studies: [`docs/benchmark-kernel.md`](docs/benchmark-kernel.md).
|
||||
|
||||
## Data safety
|
||||
|
||||
|
||||
@@ -3,9 +3,18 @@
|
||||
Remove AI provenance marks from images and video you generated yourself:
|
||||
|
||||
- known visible labels such as the Google Gemini sparkle watermark and vendor text marks;
|
||||
- invisible pixel watermarks through diffusion regeneration;
|
||||
- invisible pixel watermarks through direct local-format disruption or diffusion regeneration;
|
||||
- C2PA, EXIF, XMP, IPTC, and related AI metadata.
|
||||
|
||||
| Before | After |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
```bash
|
||||
uv tool install "remove-ai-watermarks[visible]" # CLI for visible marks and metadata
|
||||
npx skills add wiltodelta/remove-ai-watermarks # agent skill for Claude Code, Codex, Cursor, and other agents
|
||||
```
|
||||
|
||||
Video support covers provenance identification, complete visible-plus-metadata
|
||||
cleaning, directory batches, visible Sora, Veo, Seedance, Doubao, Dola,
|
||||
Hailuo AI, and Kling AI mark removal, and oracle-certified VAE regeneration for video SynthID
|
||||
@@ -24,9 +33,12 @@ removal.
|
||||
[](https://github.com/sponsors/wiltodelta)
|
||||
[](https://skills.sh/wiltodelta/remove-ai-watermarks)
|
||||
|
||||
> This project is for lawful use on content you own. It does not target stock
|
||||
> agency previews or other watermarks that protect third party paid content.
|
||||
> See [scope, safety, and legal notes](docs/legal-and-safety.md).
|
||||
> This is a research project on AI provenance signals, for lawful use on content
|
||||
> you own. It is provided as is, without warranty or liability, and each user is
|
||||
> responsible for how they use it. It does not target stock agency previews or
|
||||
> other watermarks that protect third party paid content. Some jurisdictions,
|
||||
> including China, restrict removing AI labels or supplying tools that do. See
|
||||
> [scope, safety, and legal notes](docs/legal-and-safety.md).
|
||||
|
||||
## Choose what you want to do
|
||||
|
||||
@@ -49,7 +61,9 @@ removal.
|
||||
|
||||
Microsoft Paint and Photos InvisMark declarations are routed automatically to
|
||||
pixel regeneration. The `all` command removes both the hidden pixel watermark and
|
||||
its linked C2PA manifest; metadata stripping alone removes only the manifest.
|
||||
its linked C2PA manifest; metadata stripping alone removes only the manifest. A
|
||||
specialized Python API can inspect and disrupt the validated local
|
||||
`Watermarker.dll` payload without diffusion.
|
||||
|
||||
## Installation modes
|
||||
|
||||
@@ -57,15 +71,17 @@ its linked C2PA manifest; metadata stripping alone removes only the manifest.
|
||||
| --- | --- |
|
||||
| Metadata inspection and stripping | `remove-ai-watermarks` |
|
||||
| Photograph AI-versus-camera classification | `remove-ai-watermarks[classify]` |
|
||||
| OpenAI/Google/unknown source-export classification | `remove-ai-watermarks[source-classify]` |
|
||||
| Visible detection and removal | `remove-ai-watermarks[visible]` |
|
||||
| Visible video processing | `remove-ai-watermarks[video]` |
|
||||
| Video SynthID removal | `remove-ai-watermarks[video,diffusion]` |
|
||||
| Torch-free DWT-DCT detection | `remove-ai-watermarks[detect]` |
|
||||
| Direct local Paint InvisMark disruption | `remove-ai-watermarks[pixels]` |
|
||||
| Invisible image removal (needs CUDA) | `remove-ai-watermarks[qwen-zimage]` |
|
||||
| Every production feature available on the active Python | `remove-ai-watermarks[all]` |
|
||||
|
||||
Lower-level and specialized extras include `pixels`, `heif`, `trustmark`,
|
||||
`migan`, `lama`, and `diffusion`. The
|
||||
`migan`, `lama`, `diffusion`, `classify-onnx`, and `source-classify`. The
|
||||
[installation guide](docs/installation.md#feature-extras) documents their exact
|
||||
dependency composition, Python compatibility, and model requirements.
|
||||
|
||||
@@ -93,6 +109,19 @@ remove-ai-watermarks classify image.png
|
||||
|
||||
Guide: [photo pixel classification](docs/photo-classify.md).
|
||||
|
||||
For a lightweight, abstaining OpenAI/Google/unknown source-export signal after
|
||||
metadata removal, use the separate Python API. It is not a SynthID detector:
|
||||
|
||||
```python
|
||||
import remove_ai_watermarks as raiw
|
||||
|
||||
result = raiw.classify_source("image.png")
|
||||
print(result.label, result.reason)
|
||||
```
|
||||
|
||||
Install `remove-ai-watermarks[source-classify]`. Guide:
|
||||
[source-pipeline classification](docs/source-classify.md).
|
||||
|
||||
Signed provenance is the supported route for SynthID and `identify` reads it.
|
||||
There is no local SynthID pixel detector in the package. Research on a
|
||||
periodic lattice expert is in `scripts/synthid_runtime/` and
|
||||
@@ -133,6 +162,8 @@ inspection includes the native TC260 `AIGC` tag in
|
||||
`moov.udta.meta.keys/ilst`, including a `moov` placed after the media payload,
|
||||
plus the QuickTime-form `meta` variants Doubao's iOS export writes (a bare
|
||||
`meta` box as a direct `moov` child, and a keyless `hdlr=mdir` metadata list).
|
||||
Keyed `workflow` and `prompt` entries in the same metadata-list structure are
|
||||
also detected and removed, including ComfyUI exports.
|
||||
MKV and WebM inspection reads the normative
|
||||
`Segment.Tags.Tag.SimpleTag` placement. AVI uses `LIST/INFO/AIGC`, while FLV
|
||||
uses `script.onMetaData.AIGC`. The non-ISOBMFF formats are remuxed with stream
|
||||
@@ -183,9 +214,12 @@ preserved through a timestamped in-memory NUT bridge instead of being flattened
|
||||
to the average frame rate. Non-zero source start timestamps are retained
|
||||
together with the copied audio offset. The default `--mark auto`
|
||||
scans all providers in one decode pass and selects the first stable match in
|
||||
the specificity order shown below. Pass an explicit mark to restrict detection
|
||||
to one provider.
|
||||
Sora covers the moving Sora 2 mascot and wordmark. Veo covers both the current
|
||||
the specificity order shown below. Valid Google AI-video provenance breaks a
|
||||
Sora/Veo cross-match in favor of Veo when the diamond is nested in the Sora
|
||||
region or Veo persists through the complete clip while Sora does not.
|
||||
Pass an explicit mark to restrict detection to one provider.
|
||||
Sora covers the moving Sora 2 mascot, with or without the wordmark; mascot-only
|
||||
matches also require the complete two-eye shape. Veo covers both the current
|
||||
four-point diamond and the legacy `Veo` text. Seedance covers the fixed boxed
|
||||
`AI` label, Dola covers the fixed `Dola AI` text, Hailuo AI covers the composite
|
||||
`MINIMAX | hailuo AI` label, and Kling AI covers the bottom-right `KLING AI` or
|
||||
@@ -211,8 +245,10 @@ is not a product result state. The CLI therefore describes video-pixel
|
||||
regeneration instead of claiming a per-file SynthID verdict, and reports the
|
||||
copied audio watermark as `UNVERIFIED`.
|
||||
|
||||
For invisible watermark removal, install the `qwen-zimage` extra. **An NVIDIA GPU
|
||||
is required**: all profiles are CUDA-only, and there is no CPU or MPS fallback.
|
||||
For invisible watermark removal through the CLI or high-level API, install the
|
||||
`qwen-zimage` extra. **An NVIDIA GPU is required**: all diffusion profiles are
|
||||
CUDA-only, and there is no CPU or MPS fallback. The specialized local Paint API
|
||||
uses the `pixels` extra without CUDA.
|
||||
|
||||
```bash
|
||||
uv tool install --force "remove-ai-watermarks[qwen-zimage]"
|
||||
@@ -247,11 +283,8 @@ features, and development setup.
|
||||
|
||||
### Visible Gemini mark
|
||||
|
||||
| Before | After |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
The `after` raster is generated from the tracked `before` raster by the public path:
|
||||
The `after` raster at the top of this page is generated from the tracked `before`
|
||||
raster by the public path:
|
||||
|
||||
```bash
|
||||
uv run remove-ai-watermarks visible demo_banana_before.png \
|
||||
@@ -265,8 +298,8 @@ ControlNet, followed by SAM-masked Z-Image repair of any detected face. The
|
||||
alternative, `sdxl-zimage`, swaps the global stage for SDXL and keeps the same face
|
||||
stage. A third profile, `chroma-zimage`, uses the Apache-2.0 Chroma1 global pass
|
||||
with its own flat vendor floors; see `docs/chroma1-engine-research.md` for the
|
||||
calibration. `--pipeline auto` picks chroma-zimage for Microsoft
|
||||
provenance and qwen-zimage otherwise. All are CUDA only.
|
||||
calibration. `--pipeline auto` picks sdxl-zimage for Google, chroma-zimage for
|
||||
Microsoft, and qwen-zimage otherwise. All are CUDA only.
|
||||
|
||||
```bash
|
||||
uv tool install --force "remove-ai-watermarks[qwen-zimage]"
|
||||
@@ -343,16 +376,22 @@ remove-ai-watermarks batch ./images --mode all
|
||||
Visible mark support includes:
|
||||
|
||||
- Google Gemini and Nano Banana visible sparkle watermark;
|
||||
- Doubao, the Jimeng wordmark and top-left `AI生成` pill, Qwen, Kling AI,
|
||||
Yuanbao, Baidu, LiblibAI's bottom-center wordmark and compact top-left pill,
|
||||
and RunningHub labels;
|
||||
- one calibrated Microsoft top-right white AI-badge variant;
|
||||
- one calibrated Samsung Galaxy AI label variant.
|
||||
- Doubao, the Jimeng wordmark and top-left `AI生成` layouts, Qwen, Kling AI,
|
||||
Yuanbao, Dola, WorkBuddy, Baidu, LiblibAI's bottom-center wordmark and compact
|
||||
top-left pill, and RunningHub labels;
|
||||
- calibrated Microsoft top-right white and beige AI-badge layouts;
|
||||
- calibrated Italian and Korean Samsung Galaxy AI disclosures;
|
||||
- a generic brand-less bottom-right `AI生成` fallback for OS/gallery-level
|
||||
AI-edit stamps that carry no vendor wordmark of their own (vivo and Xiaomi
|
||||
Gallery confirmed); synthetic-only calibration, weaker evidence than the
|
||||
vendor-tuned detectors above;
|
||||
- the OpenArt wordmark, placed at the frame center rather than a corner;
|
||||
uncalibrated (one confirmed real case, no captured corpus).
|
||||
|
||||
Metadata and provenance inspection covers C2PA, EXIF, XMP, IPTC, common
|
||||
generator parameters, China TC260 AIGC labels, and several vendor specific
|
||||
signals. Optional decoders add support for open DWT-DCT watermarks and Adobe
|
||||
TrustMark.
|
||||
generator parameters in image and video containers, China TC260 AIGC labels,
|
||||
and several vendor specific signals. Optional decoders add support for open
|
||||
DWT-DCT watermarks and Adobe TrustMark.
|
||||
|
||||
The exact support matrix, including important locale and detector limits, lives
|
||||
in [supported signals](docs/supported-signals.md). The
|
||||
@@ -369,13 +408,14 @@ Visible removal follows three steps:
|
||||
|
||||
Metadata removal uses format aware stripping. JPEG metadata removal preserves
|
||||
the encoded image scan instead of recompressing it. Native MP4/MOV TC260 values
|
||||
are blanked without changing box sizes or media offsets. Other supported
|
||||
containers use their corresponding metadata path.
|
||||
and keyed generation parameters are blanked without changing box sizes or
|
||||
media offsets. Other supported containers use their corresponding metadata
|
||||
path.
|
||||
|
||||
Invisible removal is different. It regenerates the image through a diffusion
|
||||
pipeline to disrupt pixel and frequency domain watermarks. This changes the
|
||||
image and cannot guarantee that a proprietary verifier will reject every
|
||||
output.
|
||||
Invisible removal through the CLI and high-level API regenerates the image through
|
||||
a diffusion pipeline. Specialized callers can instead decode and disrupt the
|
||||
positively identified local Microsoft Paint format with much smaller pixel changes.
|
||||
Neither path can guarantee that every proprietary verifier will reject every output.
|
||||
|
||||
See [supported signals](docs/supported-signals.md) and
|
||||
[known limitations](docs/known-limitations.md) for the full technical boundary.
|
||||
@@ -442,9 +482,23 @@ Claude Code can add this repository as a plugin marketplace:
|
||||
/plugin install remove-ai-watermarks@remove-ai-watermarks
|
||||
```
|
||||
|
||||
Codex reads the same skill through [`.codex-plugin/plugin.json`](.codex-plugin/plugin.json).
|
||||
The skill refuses stock-agency and other third-party paid-asset marks. See
|
||||
[the agent skill guide](docs/agent-skill.md).
|
||||
|
||||
## MCP server
|
||||
|
||||
`remove-ai-watermarks mcp` serves `identify`, `remove` and `batch` as Model
|
||||
Context Protocol tools over stdio, each a thin wrapper around one CLI command:
|
||||
|
||||
```json
|
||||
{"mcpServers": {"remove-ai-watermarks": {"command": "uvx", "args": ["remove-ai-watermarks", "mcp"]}}}
|
||||
```
|
||||
|
||||
See [the MCP server guide](docs/mcp-server.md).
|
||||
|
||||
<!-- mcp-name: io.github.wiltodelta/remove-ai-watermarks -->
|
||||
|
||||
## ComfyUI
|
||||
|
||||
The separate
|
||||
@@ -456,13 +510,15 @@ invisible removal.
|
||||
|
||||
- A missing local signal means unknown, not clean. Proprietary pixel
|
||||
watermarks may remain after metadata has been stripped.
|
||||
[What each path leaves behind](docs/known-limitations.md#what-each-path-leaves-behind)
|
||||
lists, per command, what may remain and how to check it.
|
||||
- Visible removal reconstructs a small region. Results depend on the background
|
||||
and selected fill backend.
|
||||
- Invisible removal changes the whole image and may alter faces, text, or fine
|
||||
detail.
|
||||
- Visible video removal recognizes the moving Sora 2 wordmark, the current Veo
|
||||
diamond plus legacy `Veo` text, the Seedance boxed `AI` label, and the fixed
|
||||
Doubao, Dola, Hailuo AI, and Kling AI labels. It does not recognize the older Sora Turbo
|
||||
Doubao, Dola, Hailuo AI, Vidu, and Kling AI labels. It does not recognize the older Sora Turbo
|
||||
corner swirl or unregistered layouts from those providers.
|
||||
The classical OpenCV backend can smear structured backgrounds; use MI-GAN or
|
||||
LaMa when recovery quality matters.
|
||||
@@ -470,9 +526,10 @@ invisible removal.
|
||||
The shipped profile is oracle-certified, but no public local decoder can
|
||||
certify an arbitrary output at runtime. Recheck unusually important outputs
|
||||
after provider changes.
|
||||
- Invisible-watermark removal requires CUDA. All profiles refuse any other
|
||||
device at construction rather than falling back to one that cannot run them.
|
||||
Visible removal, metadata stripping and `identify` still run anywhere.
|
||||
- Invisible-watermark removal through the CLI and high-level API requires CUDA.
|
||||
All diffusion profiles refuse any other device at construction rather than
|
||||
falling back to one that cannot run them. The specialized local Paint API,
|
||||
visible removal, metadata stripping and `identify` run without CUDA.
|
||||
- Provider watermark systems can change. Validate important outputs with the
|
||||
provider's own verifier when one is available.
|
||||
|
||||
@@ -536,7 +593,8 @@ bash maintain.sh
|
||||
```
|
||||
|
||||
See [module internals](docs/module-internals.md) before changing a subsystem
|
||||
with documented invariants.
|
||||
with documented invariants. Report vulnerabilities as described in
|
||||
[SECURITY.md](SECURITY.md).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Security policy
|
||||
|
||||
## Supported versions
|
||||
|
||||
Security fixes go into the latest release. Older releases are not patched;
|
||||
upgrade through the channel you installed from (PyPI or Homebrew).
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Report vulnerabilities privately through
|
||||
[GitHub private vulnerability reporting](https://github.com/wiltodelta/remove-ai-watermarks/security/advisories/new).
|
||||
Do not open a public issue for an unpatched vulnerability. Include the affected
|
||||
version, the command or API call, and a minimal input that reproduces it.
|
||||
|
||||
Vulnerabilities in a dependency, such as a CVE in a package from `uv.lock`, can
|
||||
be reported as a regular issue when the advisory is already public.
|
||||
|
||||
## Dependency vulnerabilities
|
||||
|
||||
Dependabot raises alerts and update pull requests for `uv.lock` and GitHub
|
||||
Actions, and CI scans `uv.lock` with
|
||||
[`uv-secure`](https://github.com/owenlamont/uv-secure) on every push and pull
|
||||
request. A vulnerable dependency is upgraded to a fixed release; advisories
|
||||
without one are tracked under
|
||||
[Known security-gate blocks](docs/development.md#known-security-gate-blocks).
|
||||
@@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="512" height="512" fill="none" stroke="url(#lg)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<!--
|
||||
The raiw.cc brand mark: the Lucide "sparkle" and "x" glyphs.
|
||||
Lucide, https://lucide.dev, ISC License, Copyright (c) Lucide Contributors.
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted, provided that the above copyright notice
|
||||
and this permission notice appear in all copies.
|
||||
-->
|
||||
<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z" fill="url(#lg)" fill-opacity="0.15"/>
|
||||
<g transform="translate(1 17) scale(0.3)" stroke-width="5">
|
||||
<path d="M18 6 6 18"/>
|
||||
<path d="m6 6 12 12"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="lg" x1="0" y1="0" x2="24" y2="24">
|
||||
<stop stop-color="#a78bfa"/>
|
||||
<stop offset="1" stop-color="#06b6d4"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -10,6 +10,11 @@ data/
|
||||
visible/ Per-mark example gallery: one synthetic canonical example per
|
||||
registered mark plus documented provider originals where available
|
||||
(see fixtures/visible/README.md)
|
||||
synthetic/ Frozen synthetic renders that unit tests stamp onto frames, each
|
||||
produced by the script named in the test that loads it
|
||||
captures/
|
||||
2026-09/ Provider outputs from the maintainer's own accounts, stored as
|
||||
served, indexed by manifest.csv (see its README)
|
||||
calibration/
|
||||
<vendor>/ Minimal controlled inputs needed to rebuild detector assets
|
||||
synthid/
|
||||
@@ -51,9 +56,15 @@ evaluation outputs outside the repository.
|
||||
`evaluations/engine-selection/` is the bounded exception for ORIGINAL study
|
||||
inputs: its manifest tracks a prompt-matched OpenAI/Meta content matrix, not
|
||||
generated model outputs or watermark-oracle evidence.
|
||||
6. Keep third-party fixture license notices in `licenses/`, outside directories
|
||||
6. Put provider captures that no test reads in `captures/<yyyy-mm>/`, with a
|
||||
`manifest.csv` row per capture. A capture that is also a fixture is stored
|
||||
once, in `fixtures/`, and its manifest row points there. Request a new
|
||||
capture only for a service x vendor pair not yet captured, or for a
|
||||
service's own model: another version of a known pair adds no provenance
|
||||
evidence.
|
||||
7. Keep third-party fixture license notices in `licenses/`, outside directories
|
||||
that tests enumerate as media inputs.
|
||||
7. Runtime detector assets belong in `src/remove_ai_watermarks/assets/`.
|
||||
8. Runtime detector assets belong in `src/remove_ai_watermarks/assets/`.
|
||||
Unregistered research candidates belong in
|
||||
`scripts/assets/visible-mark-candidates/` so they are not shipped in the
|
||||
wheel.
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Provider captures, September 2026
|
||||
|
||||
Original outputs downloaded from the maintainer's own accounts between
|
||||
2026-09-23 and 2026-09-25 and cleared for publication by the maintainer. The
|
||||
services are the Gemini API, xAI Grok, Vidu, Wan, Apple Image Playground,
|
||||
YouTube (re-encodes of our own uploads), and the paid Higgsfield, Runway and
|
||||
Dreamina plans. Every prompt is a neutral coffee-mug scene; Higgsfield
|
||||
Genjutsu and the video-edit samples take our own Higgsfield outputs as input.
|
||||
|
||||
Files are stored byte-for-byte as served. Nothing is re-encoded, because the
|
||||
provenance under study (C2PA, TC260 labels, `hf-job-id` chunks) lives in the
|
||||
container bytes.
|
||||
|
||||
`manifest.csv` lists every capture once, including the ones that are already
|
||||
test fixtures: those rows point at their `data/fixtures/` path instead of a
|
||||
second copy (`stored` is `existing`). Columns:
|
||||
|
||||
| Column | Meaning |
|
||||
| --- | --- |
|
||||
| `sha256` | Digest of the stored file |
|
||||
| `path` | Canonical repository path |
|
||||
| `stored` | `new` for files in this directory, `existing` for a fixture already tracked |
|
||||
| `service` | Service the file was downloaded from |
|
||||
| `source_name` | Local capture name, which names the model |
|
||||
| `captured` | Capture date |
|
||||
| `ai_from_metadata`, `platform` | `identify` (images) or `identify_video` (videos) verdict from metadata only, at capture time |
|
||||
|
||||
What each service does to the upstream provenance, and the per-model
|
||||
findings, are summarized in the live-captures table of
|
||||
[`docs/watermarking-landscape.md`](../../../docs/watermarking-landscape.md).
|
||||
The `google-photos/` files are the maintainer's own iPhone photos edited in
|
||||
Google Photos on iOS (an Ask edit replacing a dog with a cat, an eraser edit, and
|
||||
two other AI edits), published unmodified with their EXIF GPS position by the
|
||||
maintainer's decision on 2026-09-25. Google's checker in Gemini found SynthID in
|
||||
all four with the metadata stripped. The Apple Clean Up photo is not stored.
|
||||
The `google-photos-web/` files are a negative control from the Google Photos web
|
||||
editor (2026-09-26): one of the maintainer's photos, downloaded untouched and after
|
||||
Enhance, Sky (Vivid) and a square crop. The web editor offers no generative tool and
|
||||
writes no Content Credentials, so none of the four carries C2PA, IPTC or an AI
|
||||
credit, and Google's checker in Gemini found no SynthID in any of them (three
|
||||
explicit negatives; the crop drew a reply that did not name SynthID and is recorded
|
||||
as indeterminate).
|
||||
|
||||
- License: repository license; generated and explicitly cleared by the maintainer
|
||||
|
After Width: | Height: | Size: 6.2 MiB |
|
After Width: | Height: | Size: 6.8 MiB |
|
After Width: | Height: | Size: 7.3 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 335 KiB |
|
After Width: | Height: | Size: 407 KiB |
|
After Width: | Height: | Size: 679 KiB |
|
After Width: | Height: | Size: 636 KiB |
|
After Width: | Height: | Size: 603 KiB |
|
After Width: | Height: | Size: 627 KiB |
|
After Width: | Height: | Size: 715 KiB |
|
After Width: | Height: | Size: 529 KiB |
|
After Width: | Height: | Size: 420 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 691 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 945 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 5.0 MiB |
|
After Width: | Height: | Size: 4.7 MiB |
|
After Width: | Height: | Size: 5.9 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 4.6 MiB |
|
After Width: | Height: | Size: 4.4 MiB |
|
After Width: | Height: | Size: 4.9 MiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 6.2 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 4.9 MiB |
|
After Width: | Height: | Size: 1.6 MiB |