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.8"
|
||||
"version": "1.0.13"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -27,41 +27,67 @@ 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 walks that provider's existing order: the official API
|
||||
surface, then its Web oracle, then the remaining configured account slots.
|
||||
After a refusal, quota response, transport error, or indeterminate verdict,
|
||||
record the response and continue with the next item as its own command. Do not
|
||||
ask for confirmation before the switch. Anyone doing the task may run the
|
||||
configured checks. Do not add new logins, IP rotation, or unbounded retries.
|
||||
Do not walk a prepared route list or rotate addresses inside one call; the next
|
||||
run is a new command and picks a new session. Do not merge results from
|
||||
different sessions into one response. 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. Google's Web surfaces are `gemini-web` and `synthid-web`
|
||||
(`https://synthid.com/`). After the current surface is exhausted, record the
|
||||
response and continue with the next surface as its own command. One call does
|
||||
not upload the same file to the next surface, and the verdicts stay separate.
|
||||
|
||||
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
|
||||
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
|
||||
A signed-in account is required only for `gemini-web`. That surface uses the user's
|
||||
existing authenticated real Chrome session and an explicit `/u/N/` account index.
|
||||
Every other Web surface, including `synthid-web`, uses an isolated Playwright
|
||||
context and may name a proxy. Those contexts must never attach to or copy state
|
||||
from the user's real browser. `synthid-web` has no `/u/N/` index. Its terms forbid
|
||||
bulk verification and quota evasion, so one run submits the prepared rows once
|
||||
and does not start a sign-in. If the page says `Please sign in before detection.`
|
||||
and returns no watermark label, keep that row and continue the same prepared
|
||||
upload in the user's existing real Chrome. That Chrome session stays on the
|
||||
direct route: no proxy, no new login, and no cookie copy into Playwright. If
|
||||
that Chrome also asks for sign-in, leave its row unsubmitted. API calls
|
||||
stay on the direct SDK transport and do not take `proxy_url_env`. 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.
|
||||
A ThorData exit is not prepared ahead of time. Each unauthenticated Web run picks
|
||||
a new session and a country in Europe or the United States, and keeps that one exit
|
||||
for the browser context so page requests do not change identity. A country or
|
||||
`sessid` already written into the proxy URL is discarded. The next run picks again.
|
||||
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.
|
||||
requests alive, while Microsoft, Meta, and SynthID Detector require network idle. Preserve exact provider
|
||||
errors as indeterminate evidence, then continue with the next configured route or surface.
|
||||
|
||||
OpenAI API slots likewise carry only `api_key_env`. Store multiple key values in the
|
||||
gitignored `.env`, select one slot explicitly, and never rotate to another token after a
|
||||
refusal or rate limit. The process environment may override the matching `.env` value.
|
||||
Their SDK transport sets `trust_env=False`; environment proxy variables must not reroute
|
||||
an API check.
|
||||
gitignored `.env` and select one slot explicitly. The process environment may override
|
||||
the matching `.env` value. Their SDK transport sets `trust_env=False`, so ambient proxy
|
||||
variables do not reroute an API check, and the slot does not take `proxy_url_env`.
|
||||
After a refusal or rate limit, continue with the next configured slot in a new call,
|
||||
then with the Web surface.
|
||||
|
||||
Microsoft API slots use the Azure Content Safety and private Blob Storage resource
|
||||
identities from the gitignored `.env`. The adapter reads resource keys only into memory
|
||||
through authenticated Azure CLI, downloads and hashes the private blob before requiring
|
||||
equality with the named local source, keeps `trust_env=False`, never retries the submit,
|
||||
and reports Watermark independently from C2PA.
|
||||
equality with the named local source, keeps `trust_env=False`, and makes one submit
|
||||
inside that call. Ambient proxy settings do not apply, and the slot does not take
|
||||
`proxy_url_env`. The next slot or the Web surface is a new call and runs after a
|
||||
refusal, quota response, transport error, or indeterminate verdict. Report
|
||||
Watermark independently from C2PA.
|
||||
|
||||
Do not add an option whose only outcome is an error. Model id, step count and CFG are fixed by the profile, so none of them is a parameter of the CLI, `InvisibleEngine`, or `WatermarkRemover` -- they were accepted-then-rejected for a while, which moved the failure several frames below the caller and advertised choices the pinned stack cannot honor. If a value cannot vary, delete the knob rather than validating it.
|
||||
|
||||
@@ -73,9 +99,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 +158,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 +328,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.
|
||||
@@ -1,35 +1,65 @@
|
||||
---
|
||||
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.
|
||||
description: Run hash-bound Google, OpenAI, Microsoft, or Meta watermark-oracle checks for this repository. Use for provider verification, multi-account Gemini checks, SynthID Detector checks at synthid.com, isolated Playwright checks, or recording oracle evidence. Walk the existing plan automatically when a task needs a provider verdict.
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Provider oracles
|
||||
|
||||
Read `docs/provider-oracles.md` before acting. Use `scripts/provider_oracles.py`
|
||||
for surface discovery, slot validation, upload preparation, result recording,
|
||||
and integrity verification.
|
||||
Read `docs/provider-oracles.md` for surface keys, slot schema, preparation, and
|
||||
recording commands. Use `scripts/provider_oracles.py` for surface discovery, slot
|
||||
validation, upload preparation, result recording, and integrity verification.
|
||||
|
||||
Named slots live in the gitignored repository-local
|
||||
`.oracle-slots.json`. Never commit that file. Pass `--slot NAME` to use it;
|
||||
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. Keep each
|
||||
submitted artifact frozen inside its own batch and bind the result to that
|
||||
artifact's hash. Do not erase failures.
|
||||
|
||||
## Default surface priority
|
||||
Anyone doing the task may run the configured checks. Do not ask who is allowed
|
||||
to use the oracle, and do not ask for confirmation before moving to the next
|
||||
surface, slot, or route.
|
||||
|
||||
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`.
|
||||
## Order
|
||||
|
||||
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.
|
||||
Walk `plan PROVIDER`. That command only prints the order. The order itself stays:
|
||||
|
||||
1. The official API surface, when this tooling has one for that provider, media
|
||||
type, and configured slot. OpenAI is `openai-api`. Microsoft is
|
||||
`microsoft-api`.
|
||||
2. That provider's Web oracle. OpenAI is `openai-web` and Microsoft is
|
||||
`microsoft-web`, both through isolated Playwright. Meta is `meta-web`
|
||||
through isolated Playwright. Google is `gemini-web` on the user's existing
|
||||
real Chrome, then `synthid-web` at `https://synthid.com/` through isolated
|
||||
Playwright. Meta and Google have no API surface, so their first Web oracle is
|
||||
the first row.
|
||||
3. The remaining configured account slots of those surfaces. A signed-in
|
||||
account is required only for Gemini, and that slot stays on the direct
|
||||
route. Every other Web surface may use isolated Playwright and a proxy.
|
||||
That proxy exit is chosen when the run starts.
|
||||
|
||||
When the current surface, slot, or route is exhausted by a refusal, quota
|
||||
response, transport error, or indeterminate verdict, record the exact response
|
||||
and run the next item as its own command. One adapter call still makes one
|
||||
request.
|
||||
|
||||
## Account continuation
|
||||
|
||||
Before starting, identify the finite set of configured 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
|
||||
through the remaining slots in the order above.
|
||||
|
||||
Submit each frozen artifact at most once per slot in that pass. Preserve
|
||||
inconclusive results and disagreements between accounts. Stop when the plan is
|
||||
complete or no configured usable slot or route remains, and report remaining
|
||||
rows. Do not start a new login. Do not change candidate pixels. An API result
|
||||
is not replaced by the Web result. Run Web as its own acknowledged check and
|
||||
keep its verdict separate.
|
||||
|
||||
## Route by surface
|
||||
|
||||
@@ -44,22 +74,62 @@ 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. Upload the prepared files for the task. Pass the acknowledgement flag where
|
||||
the command requires one. 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 configured existing
|
||||
session. When no usable Gemini session remains, record that and continue with
|
||||
`synthid-web`. Do not start a new login.
|
||||
|
||||
### OpenAI, Microsoft, and Meta Web
|
||||
### SynthID Detector
|
||||
|
||||
Use the isolated Playwright runner, never the user's real Chrome:
|
||||
Use the isolated Playwright runner at `https://synthid.com/`. The page does not
|
||||
require an account login, so the slot may name a proxy. Do not start a sign-in.
|
||||
The portal's terms forbid bulk verification and quota evasion, so one invocation
|
||||
submits the prepared rows once and does not retry.
|
||||
|
||||
1. Validate a `synthid-web` slot. It does not name `account_label`,
|
||||
`browser_profile`, or `google_account_index`. It may name `proxy_url_env`.
|
||||
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. Run `run-web` with the upload acknowledgement. The runner attaches one
|
||||
prepared upload through the page's file input.
|
||||
4. Read the settled label. `Watermark detected` is `detected`. `Watermark not
|
||||
detected` and `SynthID was not detected` are `not_detected`. Quota and
|
||||
unclear wording stay `refused` or `indeterminate`. Do not click a sign-in
|
||||
button.
|
||||
5. If the anonymous page says `Please sign in before detection.` and returns no
|
||||
watermark label, keep that row. Open the same prepared upload in the user's
|
||||
existing real Chrome, where a session is already signed in. Read
|
||||
`~/Documents/GitHub/claude-config/docs/browser-automation.md` and use that
|
||||
authenticated Chrome. Do not start a new login, do not copy cookies into
|
||||
Playwright, and do not send that Chrome session through a proxy. If that
|
||||
Chrome also asks for sign-in, leave its row unsubmitted.
|
||||
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, Meta, and SynthID Detector Web
|
||||
|
||||
Use the isolated Playwright runner. Do not attach that runner to the user's
|
||||
real Chrome. The sign-in fallback in the SynthID Detector section is a separate
|
||||
session in the existing Chrome, not a proxy through this runner:
|
||||
|
||||
```bash
|
||||
uv run python scripts/provider_oracles.py run-web /outside/repo/batch/manifest.json \
|
||||
@@ -70,32 +140,43 @@ The runner is headed by default because OpenAI's Cloudflare gate blocks the
|
||||
same isolated Chromium in headless mode. Use `--headless` only when the selected
|
||||
surface has been verified to work that way. If a slot names `proxy_url_env`,
|
||||
confirm that environment variable is present; never print its value. The runner
|
||||
uses one context, one route, fresh page navigation per upload, and no retry.
|
||||
CAPTCHA, bot checks, throttling, transport failure, and unknown page wording
|
||||
remain distinct recorded outcomes.
|
||||
uses one context, one route, and fresh page navigation per upload. One
|
||||
invocation does not retry internally. CAPTCHA, bot checks, throttling, transport
|
||||
failure, and unknown page wording remain distinct recorded outcomes. After any
|
||||
of those, or after a refusal, run the next configured route or surface.
|
||||
|
||||
Wait for the surface-specific upload readiness that the runner implements.
|
||||
OpenAI completes at page load but keeps background requests alive, while
|
||||
Microsoft and Meta require network idle. Do not replace those conditions with
|
||||
Microsoft, Meta, and SynthID Detector require network idle. Do not replace those conditions with
|
||||
one shared wait. Treat OpenAI's exact `Something went wrong` response as a
|
||||
settled `indeterminate` result, not as a clean result or a reason to retry.
|
||||
settled `indeterminate` result, not as a clean result. Do not resubmit that
|
||||
same page in the same batch; continue with the next configured route or surface.
|
||||
|
||||
For ThorData residential proxy mode, keep `THORDATA_ROUTE_US`,
|
||||
`THORDATA_ROUTE_DE`, `THORDATA_ROUTE_GB`, `THORDATA_ROUTE_NL`, and
|
||||
`THORDATA_ROUTE_JP` URLs in the gitignored `.env`;
|
||||
OpenAI, Microsoft, and Meta Web slots may each name one of them through
|
||||
`proxy_url_env`. Encode an explicit two-letter ISO country code in each route,
|
||||
verify egress before relying on it, and never automatically change route after
|
||||
a refusal. Bound a batch before starting.
|
||||
A signed-in account is required only for Gemini. Every other Web surface,
|
||||
including SynthID Detector, starts in isolated Playwright and may use a proxy.
|
||||
When SynthID Detector asks for a sign-in, continue that one file in the user's
|
||||
existing real Chrome, on the direct route. An
|
||||
API call stays on the direct SDK transport and does not take a proxy.
|
||||
|
||||
A ThorData credential may live in one gitignored environment variable. Do not
|
||||
prepare a country or a session in that URL or in a slot. Each run of an
|
||||
unauthenticated Web oracle discards a country or `sessid` already present and
|
||||
picks a new session in a country in Europe or the United States. That one
|
||||
session stays for the browser context, because a ThorData endpoint without
|
||||
`sessid` rotates the exit on each request and breaks a page whose resources
|
||||
must share one network identity. The next run picks again. After a refusal,
|
||||
start that next run; do not walk a list of prepared country routes.
|
||||
|
||||
### 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
|
||||
settings. The current adapter is image-only.
|
||||
print a key or copy one into a slot or manifest. Continue through the configured
|
||||
named API slots, with a separate batch for each, and then continue to
|
||||
`openai-web`. API slots use a direct SDK transport that ignores environment
|
||||
proxy settings. The current adapter is image-only. One adapter call does not
|
||||
switch tokens; the next slot is a new call.
|
||||
|
||||
### Microsoft API
|
||||
|
||||
@@ -106,8 +187,10 @@ the gitignored `.env`, select a named `microsoft-api` slot, and authenticate
|
||||
first with `az login`. Give the Content Safety system identity `Storage Blob
|
||||
Data Reader` on the input storage account. The adapter reads resource keys only
|
||||
into memory through Azure CLI, never logs them, ignores environment proxy
|
||||
settings, makes one submit request with no retry, and polls only the returned
|
||||
operation. Keep Microsoft's Watermark and C2PA markers as separate verdicts.
|
||||
settings, makes one submit request with no retry inside that call, and polls
|
||||
only the returned operation. After that call is refused, hits quota, fails, or
|
||||
returns indeterminate, continue with another `microsoft-api` slot or with
|
||||
`microsoft-web`. Keep Microsoft's Watermark and C2PA markers as separate verdicts.
|
||||
|
||||
## Evidence rules
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
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
|
||||
@@ -82,7 +84,13 @@ finding.
|
||||
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.
|
||||
External oracle uploads use the `provider-oracles` skill. When a cohort needs a
|
||||
provider verdict, walk that skill's plan automatically: API first, then the Web
|
||||
oracle, including `synthid-web` at synthid.com for Google, then the remaining
|
||||
configured account slots. A signed-in account is required only for Gemini.
|
||||
Every other Web surface, including `synthid-web`, may use isolated Playwright
|
||||
and a proxy, and each proxied run picks a new session in Europe or the United
|
||||
States. If SynthID Detector asks for a sign-in, continue that file in the user's
|
||||
existing real Chrome on the direct route, and do not start a new login. Do not ask for
|
||||
confirmation before switching, and do not adapt candidates against an oracle.
|
||||
Do not substitute one provider's verdict for another's.
|
||||
@@ -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 }}
|
||||
@@ -213,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 }}
|
||||
|
||||
@@ -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
|
||||
@@ -36,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"
|
||||
|
||||
|
||||
@@ -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. 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).
|
||||
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. Scanner failures are fatal too. The gate also runs `scripts/sync_c2pa_soft_bindings.py --check`, which fails when the packaged C2PA soft-binding registry snapshot is stale; rerun it without `--check` to refresh the snapshot from upstream. 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
|
||||
@@ -23,10 +32,14 @@ removal.
|
||||
[](https://github.com/wiltodelta/remove-ai-watermarks/actions/workflows/test.yml)
|
||||
[](https://github.com/sponsors/wiltodelta)
|
||||
[](https://skills.sh/wiltodelta/remove-ai-watermarks)
|
||||
[](https://glama.ai/mcp/servers/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 +62,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 +72,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`, `diffusion`, and `classify-onnx`. 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 +110,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
|
||||
@@ -185,9 +215,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
|
||||
@@ -213,8 +246,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]"
|
||||
@@ -249,11 +284,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 \
|
||||
@@ -267,8 +299,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]"
|
||||
@@ -345,11 +377,17 @@ 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 in image and video containers, China TC260 AIGC labels,
|
||||
@@ -375,10 +413,10 @@ 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.
|
||||
@@ -445,9 +483,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
|
||||
@@ -459,13 +511,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.
|
||||
@@ -473,9 +527,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.
|
||||
|
||||
@@ -539,7 +594,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 |