
watermarks-remover v0.5.0
Un'app che mette al primo posto la privacy e rimuove le filigrane AI dai contenuti di tua proprietà.
_ _ _ ____ ___ ____ ____ _ _ ____ ____ _ _ ____ ____ ____ _ _ ____ _ _ ____ ____
| | | |__| | |___ |__/ |\/| |__| |__/ |_/ [__ __ |__/ |___ |\/| | | | | |___ |__/
|_|_| | | | |___ | \ | | | | | \ | \_ ___] | \ |___ | | |__| \/ |___ | \
watermarks-remover
Skill per agent + servizio Python basato sulla stdlib per rimuovere marcature di provenienza AI multi-vendor da testo e file — per privacy e igiene su contenuti di tua proprietà. Lo skill è un thin client: pilota il meccanismo via HTTP, quindi l'host dell'agent non necessita di Python.
| Livello | Obiettivo | Come |
|---|---|---|
| A | Unicode invisibile, spazi esotici, bidi, tag chars | Script Python deterministici |
| B | Watermark testuali statistici (token-sampling) | Riscrittura dell'agent + hook opzionale rewrite_text.py |
| File | C2PA / EXIF / XMP / proprietà documento | PNG, JPEG, WebP, AVIF, HEIC, BMP, GIF, TIFF, SVG, PDF, DOCX, XLSX, PPTX, EPUB, ODT, HTML, Markdown, MP4/MOV/M4A/M4V, WAV, MP3, FLAC |
Vendor / ecosistemi (a livello di classe): Claude, Gemini / SynthID-Text, superfici di provenienza OpenAI, marcature open-LLM in stile Kirchenbauer (green-list) e keyed-Gumbel / EXP (Aaronson).
Ultima release: v0.7.0
Percorso dello skill: skills/remove-ai-marks/
Percorso del servizio: service/
(migrazione: precedentemente remove-claude-marks; l'alias slash /remove-claude-marks è ancora documentato)
Installazione (skill per agent)
Lo skill non include codice — chiama il servizio via HTTP. Installa lo skill (solo markdown) e avvia il servizio, poi imposta WATERMARKS_SERVICE_URL se non è http://127.0.0.1:8765.
In Claude Code, la via più rapida è il plugin marketplace incluso — nessun clone, e si aggiorna sul posto. Ovunque altro, un singolo installer copre ogni host supportato (Python 3.10+ stdlib, nessuna dipendenza):```bash python3 install_skill.py --skill remove-ai-marks --target claude-code
| Host | Target | Arriva in |
| --- | --- | --- |
| Claude Code (personale) | `--target claude-code` | `~/.claude/skills/<skill>` (rispetta `CLAUDE_CONFIG_DIR`) |
| Claude Code (progetto) | `--target claude-project --project-dir PATH` | `PATH/.claude/skills/<skill>` |
| Cowork, claude.ai, sessioni cloud, routine | `--target cowork` | `dist/<skill>.zip` da caricare in **Customize → Skills** |
| Cursor | `--target cursor` (predefinito) | `~/.cursor/skills/<skill>` |
Skill distribuite: `remove-ai-marks` (completa, con supporto del servizio) e
`clean-user-facing-text` (solo testo, autonoma). `--list` le stampa.
Le installazioni esistenti vengono preservate a meno che non si passi `--force`; la sostituzione viene
preparata prima e l'installazione precedente viene conservata come backup con nome univoco.
`--link` crea un symlink a questo checkout invece di copiare, così le modifiche vengono recepite
in tempo reale. Su Windows, usa `py install_skill.py ...`; il wrapper `install-skill.sh` è
fornito per le shell macOS/Linux.
Prima di scrivere qualsiasi cosa, l'installer convalida la skill rispetto alle
regole di packaging [Agent Skills](https://agentskills.io) che gli upload su claude.ai
e la Skills API applicano: frontmatter conforme alla specifica (`name`, `description`,
`license`, `compatibility`, `metadata`, `allowed-tools`), un `name` in minuscolo con trattini
di al massimo 64 caratteri corrispondente alla directory, una `description` non vuota
di al massimo 1024 caratteri. Il bundle Cowork deve inoltre
rispettare il limite di upload di 30 MB, che il packager applica.
### Pulizia automatica tramite hook (deterministica)
Una skill è un'istruzione: il modello decide se invocarla, e il
modello è ciò che produce i marchi. Un **hook** viene eseguito dall'harness
a ogni chiamata di tool corrispondente, senza bisogno di cooperazione. Questo rende l'hook la
metà deterministica di questo flusso di lavoro.
Il plugin registra un hook `PostToolUse` su `Write|Edit|MultiEdit|NotebookEdit`
che esegue [`service/scripts/hook_written_file.py`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/service/scripts/hook_written_file.py)
sul file appena scritto dall'agente. Due modalità, che seguono la convenzione pre-commit
del controllo predefinito:
| Modalità | Comportamento |
| --- | --- |
| `check` (predefinita) | Segnala i marchi di provenienza, lascia il file invariato. I risultati vanno al modello (exit 2), così può proporre di pulirli. |
| `clean` | Rimuove i marchi sul posto, poi informa il modello che il file su disco è cambiato. |
Imposta la modalità dalle impostazioni del plugin (**Hook mode** in `/plugin manage`,
letta dall'hook come `CLAUDE_PLUGIN_OPTION_HOOK_MODE`), oppure con
`WATERMARKS_HOOK_MODE=clean` nell'ambiente. Il comando dell'hook deliberatamente
**non** interpola `${user_config.hook_mode}`: Claude Code rifiuta di eseguire
un hook che fa riferimento a un'opzione che l'utente non ha mai aperto `/plugin manage` per
impostare — un `default` dichiarato non lo soddisfa — quindi interpolarlo significherebbe
che l'hook non verrebbe mai eseguito silenziosamente su un'installazione nuova. Il rilevamento riutilizza
`scan_file` / `is_actionable` di `audit_lib`, così l'hook, il gate pre-commit e l'esportazione
SARIF della CI concordano su cosa conta come actionable; la pulizia richiama
`clean_file.py`, così nessuna logica di pulizia viene duplicata. La modalità `clean` scrive su un
file temporaneo adiacente e scambia solo in caso di differenza reale, così i file che erano
già puliti mantengono il loro mtime e non ritriggerano i file watcher.
Senza il plugin, collegalo tu stesso in `~/.claude/settings.json` (o in un
`.claude/settings.json` di progetto):```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit|NotebookEdit",
"hooks": [
{
"type": "command",
"command": "python3",
"args": ["/path/to/watermarks-remover/service/scripts/hook_written_file.py",
"--mode", "check"],
"timeout": 30
}
]
}
]
}
}
Su Windows, sostituisci python3 con py.
Cosa non può fare un hook. Nessun hook può riscrivere il messaggio di chat dell'assistente
prima che tu lo legga. L'hook Stop di Claude Code riceve last_assistant_message
in sola lettura, e non esiste un filtro pre-invio per le risposte finali — lo stesso limite
che questo progetto documenta già per le regole di Cursor. Quindi la garanzia
deterministica copre i file che l'agente scrive, più il
gate pre-commit per qualsiasi cosa in procinto di entrare in git. Il testo che
esiste solo nel transcript della chat dipende ancora dal flusso di lavoro della skill,
che si basa su istruzioni del modello ed è quindi best-effort.
Plugin Claude Code (marketplace)
Il repository è anche un plugin Claude Code e un
marketplace a plugin singolo (.claude-plugin/), quindi entrambe le skill si installano e si aggiornano in due
comandi, senza necessità di clone o script:```
/plugin marketplace add guillaumemeyer/watermarks-remover
/plugin install watermarks-remover@watermarks-remover
Le skill vengono quindi caricate con namespace: `/watermarks-remover:remove-ai-marks` e
`/watermarks-remover:clean-user-facing-text` (anche il semplice `/remove-ai-marks`
funziona quando nient'altro rivendica il nome). `/plugin marketplace update
watermarks-remover` scarica le versioni successive. Lo stesso funziona dalla CLI con
`claude plugin marketplace add …` / `claude plugin install …`, e da un checkout locale
passando un percorso invece di `owner/repo`.
Manutentori: `make plugin-validate` esegue `claude plugin validate . --strict`
su entrambi i manifest; `tests/test_plugin_manifest.py` copre gli stessi file
senza bisogno della CLI.
### Claude Code```bash
# Personal — available in all your projects
python3 install_skill.py --skill remove-ai-marks --target claude-code
# or: make install-claude-code-skill
# Project — commit .claude/skills/ to share it with the repo
python3 install_skill.py --skill remove-ai-marks --target claude-project \
--project-dir /path/to/project
# or: make install-claude-project-skill PROJECT=/path/to/project
Claude Code rileva le competenze personali e di progetto senza riavvio; /skills
elenca quelle caricate. Richiama con /remove-ai-marks o chiedi di "rimuovere
watermark AI / C2PA / marchi Claude / testo di classe SynthID". Un'installazione di progetto è
anche ciò che leggono le sessioni cloud,
poiché clonano il repository e caricano la sua .claude/skills/.
Cowork (e claude.ai, sessioni cloud, routine)
Le sessioni Cowork non leggono ~/.claude/skills sulla tua macchina — caricano
le competenze abilitate per il tuo account claude.ai, sincronizzate all'avvio della sessione.
Quindi installa lì caricando un bundle:```bash
python3 install_skill.py --skill remove-ai-marks --target cowork
writes dist/remove-ai-marks.zip (make package-cowork-skill)
Poi, nell'app Claude Desktop, apri **Customize → Skills → Add** e carica
lo zip (le stesse impostazioni delle skill su claude.ai funzionano allo stesso modo). Il bundle è
riproducibile e contiene una singola directory di primo livello `remove-ai-marks/` con
`SKILL.md` alla sua radice, che è il layout che l'upload si aspetta.
La raggiungibilità del servizio conta più qui che in un'installazione locale: la skill è un
sottile client HTTP, quindi la sessione deve essere in grado di raggiungere `WATERMARKS_SERVICE_URL`.
Le sessioni Cowork che vengono eseguite localmente sulla tua macchina raggiungono un `make serve` locale;
le sessioni cloud e le routine vengono eseguite da remoto e necessitano di un URL del servizio raggiungibile da
lì (e di `WATERMARKS_SERVER_API_KEY` impostata su di esso). Se vuoi una skill senza alcun
servizio, carica invece `clean-user-facing-text` — è solo testo e
include i propri script:```bash
python3 install_skill.py --skill clean-user-facing-text --target cowork
Grok```bash
Grok Build / project-local
mkdir -p .grok/skills ln -sfn "$(pwd)/skills/remove-ai-marks" .grok/skills/remove-ai-marks
User-global Grok
mkdir -p ~/.grok/skills ln -sfn "$(pwd)/skills/remove-ai-marks" ~/.grok/skills/remove-ai-marks
### Skill opzionale solo testo
[`skills/clean-user-facing-text/`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/skills/clean-user-facing-text) è uno
skill autonomo per manoscritti autorizzati, documentazione e testi
web. Esclude strumenti per immagini, C2PA, servizi e modelli esterni, e
esegue i propri script Layer A integrati invece di chiamare il servizio.```bash
python3 install_skill.py --skill clean-user-facing-text --target claude-code
python3 install_skill.py --skill clean-user-facing-text --target cursor
L'invocazione delle skill è selezionata dal modello. I progetti che adottano esplicitamente questo flusso di lavoro in Cursor possono anche copiare la regola opzionale:```bash
mkdir -p /path/to/project/.cursor/rules
cp integrations/cursor/clean-user-facing-text.mdc
/path/to/project/.cursor/rules/clean-user-facing-text.mdc
Per tutti i progetti, inserisci invece la stessa istruzione nelle **User Rules** di Cursor.
Le regole migliorano la coerenza ma rimangono istruzioni per il modello; Cursor non espone
un filtro deterministico pre-invio per le risposte finali in chat.
### Avviare il servizio
Il percorso più rapido è un server HTTP locale (solo Python 3.10+ stdlib — nessuna dipendenza, nessun Docker):```bash
make serve # http://127.0.0.1:8765
# or directly:
python3 service/scripts/server.py --host 127.0.0.1 --port 8765
Windows (senza Docker)
Vedi docs/windows-autostart.md per l'avvio automatico del servizio all'accesso a Windows senza Docker.
Per l'intera infrastruttura (core + backend opzionali harness/heavy), vedi Docker / compose di seguito.
Strumenti di sistema opzionali (usati automaticamente quando presenti — preinstallati nell'immagine Docker core):
| Strumento | Ruolo |
|---|---|
c2patool | Ispeziona i manifest C2PA |
exiftool | Rimozione dei metadati residui (spec. PDF) |
qpdf | Ricostruzione strutturale del PDF — richiesto per una vera rimozione dei metadati PDF (vedi sotto) |
Gli script core richiedono solo la stdlib di Python 3.10+. Le chiamate ai modelli del Layer B sono opzionali.
Uso rapido (script)```bash
SCRIPTS=service/scripts
Unified inspect / clean
python3 "$SCRIPTS/inspect_file.py" draft.md python3 "$SCRIPTS/clean_file.py" draft.md -o draft.cleaned.md python3 "$SCRIPTS/clean_file.py" photo.png -o photo.cleaned.png python3 "$SCRIPTS/clean_file.py" notes.docx -o notes.cleaned.docx
Text Layer A
python3 "$SCRIPTS/inspect_text.py" draft.md python3 "$SCRIPTS/clean_text.py" draft.md -o draft.cleaned.md --stats
Layer B rewrite hook (default: print prompt only — no model required)
python3 "$SCRIPTS/rewrite_text.py" draft.md --backend print-prompt --tactic paraphrase
Optional local Ollama (loopback only by default — remote endpoints require
WATERMARKS_REWRITE_ALLOW_REMOTE=1 or --allow-remote):
WATERMARKS_REWRITE_BACKEND=ollama WATERMARKS_REWRITE_MODEL=llama3.2 \
python3 "$SCRIPTS/rewrite_text.py" draft.md -o draft.rewritten.md
API keys are read from WATERMARKS_REWRITE_API_KEY only (never argv).
Images
python3 "$SCRIPTS/inspect_image.py" shot.png python3 "$SCRIPTS/clean_image.py" shot.png -o shot.cleaned.png
### Gli strumenti per il testo rifiutano input binari
`inspect_text.py`, `clean_text.py` e `rewrite_text.py` operano su testo. Se puntati
a un `.docx`, `.pdf` o immagine, decodificavano i byte compressi e riportavano
qualsiasi codepoint ne risultasse — rumore che segue la compressione, non il
contenuto — e `clean_text.py` riscriveva poi quei byte corrotti, distruggendo il
file. Ora rifiutano input binari e indicano lo strumento che li gestisce:```bash
python3 "$SCRIPTS/inspect_text.py" report.docx
# refusing to treat report.docx as text: it looks like a ZIP container (DOCX, ODT, …).
# Use inspect_file.py / clean_file.py, which route by format,
# or pass --force-text to scan the raw bytes anyway.
Il rilevamento avviene tramite magic number più un rapporto di byte di controllo, quindi il testo in codifiche diverse da UTF-8 continua a funzionare. --force-text lo sovrascrive ovunque.
I formati non riconosciuti non vengono mai puliti automaticamente
classify() etichetta i byte che non corrispondono ad alcun formato di testo, immagine o contenitore supportato come unknown — non ripiega più su "text". In modalità automatica clean_file.py rifiuta tali file (uscita 2, nessun output scritto) invece di decodificarli come UTF-8 e riscrivere byte manomessi; --as text o --force-text sono le opzioni esplicite di attivazione. inspect_file.py segnala il file come unknown (uscita 0), e il servizio HTTP risponde a /inspect con kind: "unknown" ma rifiuta /clean di formati sconosciuti (400 — inviare un nome file con un'estensione nota, ad es. notes.txt).
Servizio HTTP
La stessa macchina funziona come servizio HTTP della libreria standard (service/scripts/server.py) — l'interfaccia che la skill utilizza e il modo in cui qualsiasi app web può integrarsi senza vendorizzare:
| Metodo | Percorso | Corpo | Restituisce |
|---|---|---|---|
| GET | /health | — | {"ok": true, "version": ...} |
| GET | /capabilities | — | strumenti / backend opzionali utilizzabili (ogni strumento è verificato nella versione, non solo trovato nel PATH) |
| GET | /openapi.json | — | specifica OpenAPI 3.0.3 generata dinamicamente |
| POST | /inspect | {"file": "<base64>", "name": "notes.md"} | {"ok", "kind", "suspicious", "report"} |
| POST | /detect | {"file": "<base64>", "name": "notes.txt"} | {"ok", "kind", "detections": [...]} |
| POST | /clean | {"file": "<base64>", "name": "notes.md", "options": {...}} | {"ok", "kind", "cleaned": "<base64>", "report"} |
| POST | /watermark | {"text": "...", "keys": [118, 504, ...], "options": {...}} o {"file": "<base64>", ...} | {"ok", "kind", "watermarked_text", "report": {"scheme_used", ...}} |
| POST | /inspect/batch | {"files": [{"file": "<base64>", "name": "notes.md"}, ...]} | {"ok", "results": [{"name", "ok", "kind", "suspicious", "report"}, ...]} |
| POST | /detect/batch | {"files": [{"file": "<base64>", "name": "notes.txt"}, ...]} | {"ok", "results": [{"name", "ok", "kind", "detections", "report"}, ...]} |
| POST | /clean/batch | {"files": [{"file": "<base64>", "name": "notes.md", "options": {...}}, ...]} | {"ok", "results": [{"name", "ok", "kind", "cleaned", "report"}, ...]} |
| POST | /watermark/batch | {"files": [{"text": "...", "keys": [...]}, {"file": "<base64>"}, ...]} | {"ok", "results": [{"name", "ok", "kind", "watermarked_text", "report": {"scheme_used", ...}}, ...]} |
Gli endpoint batch eseguono in ciclo la stessa pipeline per file di /inspect, /detect, /clean e /watermark, con un limite di WATERMARKS_MAX_BATCH_FILES file per richiesta (predefinito 50). Una voce malformata (base64 errato, opzione sconosciuta, formato non riconosciuto) si manifesta come "ok": false di quella voce con una stringa "error" — non interrompe mai il resto del batch.```bash
WM="http://127.0.0.1:8765"
curl -s "$WM/health" # {"ok": true, "version": "..."}
curl -s "$WM/openapi.json" # machine-readable OpenAPI 3.0.3 contract
curl -s -X POST "$WM/clean" -H 'Content-Type: application/json'
-d "{"file": "$(base64 < notes.md | tr -d '\n')", "name": "notes.md"}"
Il servizio instrada in base all'estensione del file e poi ai magic bytes, quindi testo / immagine / container vengono rilevati automaticamente. Imposta `WATERMARKS_SERVER_API_KEY` per richiedere `Authorization: Bearer <key>` su ogni richiesta. Bind predefinito solo su loopback (`--host` per sovrascrivere); pensato per una rete fidata.
### Rilevamento watermark (`/detect` e `detect_before` / `detect_after`)
Il rilevamento è un passaggio separato dalla pulizia — il servizio non chiama mai
API di fornitori a meno che tu non glielo chieda:
- **`POST /detect`** esegue i rilevatori di watermark configurati su un file.
Testo → rilevatori di fornitori + stilometria; immagine → punteggio pixel SynthID.
- **`/inspect`** accetta un flag opt-in `"detect": true` che aggiunge
i risultati dei rilevatori al report testuale (e può impostare `suspicious`).
- **`/clean`** accetta le opzioni `"detect_before"` / `"detect_after"` per
valutare l'input e l'output pulito, così puoi misurare cosa ha
effettivamente cambiato una pulizia.
- **`/clean`** esegue la riscrittura del testo Layer B dopo il Layer A **per impostazione predefinita** (è
un passaggio obbligatorio per il testo). Un'opzione **`"strategy"`** (una lista ordinata
`tactic@intensity`, ad es. `"[email protected],[email protected]"`) sovrascrive il
valore predefinito dal file di configurazione della strategia (vedi sotto). Quando il backend/modello di riscrittura
per un passaggio non è configurato, `/clean` restituisce un 400.
Rilevatori di testo (vedi `/capabilities` → `text_detectors`):
Rilevatori di testo (vedi `/capabilities` → `text_detectors`):
| Rilevatore | Attivato da | Note |
| --- | --- | --- |
| `markllm` | `MARKLLM_DIR` (checkout host) | Harness di ricerca (schemi KGW / SynthID), solo stessa configurazione — non un oracolo di fornitore. |
| `gumbel` | `WATERMARKS_GUMBEL_KEY` | Replay same-key senza modello dello schema keyed-Gumbel (Aaronson EXP) (vedi `detect_gumbel.py`), solo stdlib — motori self-hosted come arbi-serve; solo stessa chiave, non un oracolo di fornitore. |
| `claude-text` | — (segnaposto) | Anthropic ha annunciato un'API di rilevamento watermark; questa interfaccia si attiverà quando sarà disponibile. |
Punteggio immagini: quando `WATERMARKS_SYNTHID_SCORER_URL` è impostato, il servizio
valuta le immagini tramite il sidecar `wr-synthid-score` (profilo heavy); con un
`REVERSE_SYNTHID_DIR` locale usa direttamente il checkout. Il rilevamento è
fail-soft: rilevatori non configurati, scaduti o in errore riportano
`{"available": false, "error": ...}` e non bloccano mai la pulizia.
### Generazione watermark (`/watermark` e `/watermark/batch`)
Genera testo con watermark per la valutazione benchmark e i test round-trip.
Quando `WATERMARKS_SYNTHID_TEXT_URL` è impostato, il servizio delega la generazione al
sidecar `wr-synthid-text` (profilo harness); con un `MARKLLM_DIR` locale usa il
checkout direttamente. Come il rilevamento, la generazione è fail-soft: un generatore non configurato
riporta `{"ok": false, "error": ...}`.
## Docker / compose
Immagini pubblicate (GHCR):
| Tag immagine | Contenuto | Pubblicata? |
| --- | --- | --- |
| `ghcr.io/guillaumemeyer/watermarks-remover:<tag>` / `:latest` | Servizio HTTP core + tutti i cleaner + exiftool / qpdf / c2patool | Sì |
| `…:markllm-<tag>` / `:markllm-latest` | Harness watermark testo MarkLLM (upstream Apache-2.0) | Sì |
| `…:markdiffusion-<tag>` / `:markdiffusion-latest` | Harness immagini MarkDiffusion (upstream Apache-2.0) | Sì |
| `watermarks-remover-ctrlregen:local` | Rimozione pixel CtrlRegen — **mai pubblicata** (`noai-watermark` non include LICENSE) | Solo build locale |
| `watermarks-remover-synthid-scorer:local` | scorer reverse-SynthID — **mai pubblicata** (Research License non commerciale) | Solo build locale (scorer CLI + sidecar HTTP opzionale `wr-synthid-score` sotto il profilo `heavy`) |
Compila ed esegui il servizio core:```bash
make docker-core-build
docker run --rm -p 127.0.0.1:8765:8765 --read-only --tmpfs /tmp watermarks-remover
# any CLI stays runnable by overriding the command:
docker run --rm -v "$(pwd):/data" watermarks-remover \
/app/scripts/clean_file.py /data/notes.md -o /data/notes.cleaned.md
Bring-up dell'intera infrastruttura:```bash docker compose up -d # core HTTP service only docker compose --profile harness up -d # + markllm / markdiffusion / wr-synthid-text sidecar docker compose --profile heavy up -d # + ctrlregen / synthid (local builds) docker compose --profile harness --profile heavy up -d # all services
Lo stack compose mappa il servizio core su `127.0.0.1:8765`. I servizi persistenti vengono eseguiti come daemon in background (`wr-core` e il sidecar `wr-synthid-text` sotto il profilo harness). I restanti servizi harness/heavy sono CLI one-shot — invocali con `docker compose run --rm <service> …` quando hai bisogno di verifica o lavoro sui pixel.
Convalida lo stack in esecuzione (solo codice di uscita, nessun output in caso di successo):```bash
make compose-check # or: ./compose-check.sh
Verifica wr-core tramite GET /health ed esegue ogni harness/servizio pesante con --help, richiedendo exit 0.
Configurazione (variabili d'ambiente per docker compose)
La pulizia del testo richiede la configurazione del Layer B — la riscrittura del Layer B è
un passaggio obbligatorio per POST /clean sul testo, quindi il servizio core necessita del backend di riscrittura
configurato, altrimenti la pulizia del testo restituisce HTTP 400. La pulizia dei metadati di immagini/container
funziona out of the box. Per il testo è necessario configurare le dipendenze della strategia Layer B:
transformers + roberta-large (per il passo mlm predefinito) e
la configurazione LLM WATERMARKS_REWRITE_* (per il passo paraphrase):```bash
echo "Hello\u200bWorld\u00ad!" > /tmp/sample.txt
curl -s -X POST http://127.0.0.1:8765/clean -H 'Content-Type: application/json'
-d "{"file": "$(base64 < /tmp/sample.txt | tr -d '\n')", "name": "sample.txt"}"
Le lingue la cui tipografia si basa su uno spazio unificatore (francese `« … »`, lo
spazio prima di `; : ! ?`) dovrebbero passare `"options": {"normalize_spaces": false}`, l'
equivalente HTTP di `clean_text.py --no-normalize-spaces`. I carrier invisibili vengono
comunque rimossi; viene saltata solo la riscrittura degli spazi.
Tutto il resto è opzionale e risiede in un file `.env` nella radice del repo. `docker compose` **carica automaticamente `.env`** e da esso interpola i riferimenti `${VAR}` in `compose.yaml` (le esportazioni della shell hanno la precedenza su `.env` se entrambi sono impostati).```bash
cp .env.example .env # then edit
docker compose up -d # picks up .env automatically
.env è gitignored (deny-by-default) — non committarlo mai. Per le esecuzioni CLI lato host (rewrite_text.py, la skill), esporta lo stesso file nell'ambiente:```bash
set -a; . ./.env; set +a; python3 service/scripts/rewrite_text.py /tmp/x.txt -o /tmp/x.rewritten.txt
| Var | Reaches | Purpose |
| --- | --- | --- |
| `WATERMARKS_SERVER_API_KEY` | `wr-core` (via compose `environment`) | Richiede `Authorization: Bearer <key>` sull'API HTTP |
| `WATERMARKS_GEMINI_*` | — | Rimosso Ago 2026: Google ha ritirato il watermarking testuale SynthID sull'API (vedi `vendor-notes.md`) |
| `WATERMARKS_SYNTHID_SCORER_URL` | `wr-core` | Punta il core al sidecar `wr-synthid-score` per lo scoring delle immagini SynthID (es. `http://wr-synthid-score:8766` sotto il profilo heavy) |
| `WATERMARKS_SYNTHID_SCORER_API_KEY` | `wr-core` + `wr-synthid-score` | Chiave bearer condivisa per il sidecar scorer (vuota = nessuna autenticazione) |
| `WATERMARKS_SYNTHID_TEXT_URL` | `wr-core` | Punta il core al sidecar `wr-synthid-text` per il watermarking testuale SynthID (es. `http://wr-synthid-text:8767` sotto il profilo harness) |
| `WATERMARKS_SYNTHID_TEXT_API_KEY` | `wr-core` + `wr-synthid-text` | Chiave bearer condivisa per il sidecar di watermarking testuale (vuota = nessuna autenticazione) |
| `WATERMARKS_SYNTHID_TEXT_TIMEOUT` | `wr-core` | Secondi di attesa per il sidecar `wr-synthid-text` (default 120) |
| `WATERMARKS_MARKLLM_SCHEME` | `text_detectors.py` (host) | Schema MarkLLM per `/detect`: `kgw` (default) / `synthid` |
| `HF_TOKEN` | servizi harness/heavy | Token Hugging Face per modelli ad accesso limitato |
| `WATERMARKS_SERVICE_URL` | solo client (skill / curl) | Dove raggiungere il servizio; default `http://127.0.0.1:8765` |
| `WATERMARKS_REWRITE_BACKEND` | hook `rewrite_text.py` | `print-prompt` (default) / `ollama` / `openai-compatible` |
| `WATERMARKS_REWRITE_MODEL` | hook `rewrite_text.py` | Nome del modello (es. `deepseek-v4-flash`) |
| `WATERMARKS_REWRITE_BASE_URL` | hook `rewrite_text.py` | Base API (es. `https://api.deepseek.com`) |
| `WATERMARKS_REWRITE_API_KEY` | hook `rewrite_text.py` | Chiave API — solo da env, mai su argv |
| `WATERMARKS_REWRITE_ALLOW_REMOTE` | hook `rewrite_text.py` | `1` per consentire endpoint non-loopback |
| `WATERMARKS_REWRITE_REASONING_EFFORT` | hook `rewrite_text.py` | `none` (default) / `low` / `medium` / `high` / `off` |
| `WATERMARKS_CLEAN_STRATEGY_FILE` | `server.py` `/clean` | Percorso del file JSON di configurazione della strategia Layer B (default `config/clean_strategy.json`) |
| `WATERMARKS_GUMBEL_KEY` | `detect_gumbel.py` / `text_detectors.py` | Chiave segreta per il replay same-key di keyed-Gumbel (EXP) (es. `0x…`); preferita rispetto ad argv — mai registrata nei log |
**Il Layer B è necessario per la pulizia del testo.** `/clean` applica sempre la
strategia predefinita (da `config/clean_strategy.json`, `{"default_strategy": "[email protected],[email protected]"}`) a un file di testo dopo il Layer A, a meno che la richiesta non passi la propria opzione `"strategy"` (una lista ordinata `tactic@intensity`). Un passo della strategia è `tactic@intensity`; il passo `mlm` richiede `transformers` + `roberta-large`, e qualsiasi passo LLM (`paraphrase`, `humanize`, …) richiede la configurazione `WATERMARKS_REWRITE_*`. Se il backend/modello richiesto non è configurato — o nessuna strategia è disponibile — `/clean` **rifiuta la richiesta con un 400**. Precedenza per il percorso di configurazione: flag CLI `--strategy-config` > variabile d'ambiente `WATERMARKS_CLEAN_STRATEGY_FILE` > il default `config/clean_strategy.json`.
Le immagini vengono pubblicate automaticamente sui tag `v*` tramite [`.github/workflows/release-images.yml`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/.github/workflows/release-images.yml).
## Scoring opzionale dei pixel SynthID
`inspect_image.py` e `clean_image.py` possono riportare un punteggio di confidenza
SynthID nel dominio dei pixel quando è disponibile un checkout esterno di
[`aloshdenny/reverse-SynthID`](https://github.com/aloshdenny/reverse-SynthID).
Lo scorer **non è incluso nel bundle**: viene caricato a runtime dal tuo
checkout, e il suo codice rimane sotto la licenza Research non commerciale
del progetto upstream.
### Opzione 1: bootstrap con un solo comando (senza Docker)```bash
SCRIPTS=service/scripts
# Clones upstream, creates a venv, and installs scorer-only dependencies.
"$SCRIPTS/setup_synthid.sh"
# Score an image (default checkout: ~/reverse-SynthID).
REVERSE_SYNTHID_DIR=~/reverse-SynthID \
~/reverse-SynthID/.venv/bin/python "$SCRIPTS/score_synthid.py" shot.png
# Or surface the score from inspect / clean (same venv Python).
REVERSE_SYNTHID_DIR=~/reverse-SynthID \
~/reverse-SynthID/.venv/bin/python "$SCRIPTS/inspect_image.py" shot.png
setup_synthid.sh accetta --dir PATH, --ref REF e --full (installa il
requirements.txt upstream completo, che aggiunge torch/diffusers per il
bypass VAE upstream che questo progetto non utilizza).
Su Windows usare setup_synthid.ps1 (-Dir, -Ref, -Full), che crea il
venv in .venv\Scripts\ — il layout che image_meta.py cerca già su
os.name == "nt".
Opzione 2: build Docker locale```bash
make docker-synthid-build
Run unprivileged and with a read-only rootfs; the scorer only needs to read
/data and write to stdout/tmp.
docker run --rm
--user "$(id -u):$(id -g)"
--read-only --tmpfs /tmp
-v "$(pwd):/data"
watermarks-remover-synthid-scorer /data/shot.png
L'immagine viene compilata localmente dal sorgente upstream al momento della build. Non viene
pubblicata, quindi non ridistribuisce il codice upstream.
### Opzione 3: sidecar HTTP scorer (docker compose)
Nel profilo `heavy` lo stack compose esegue anche lo scorer come sidecar
HTTP (`wr-synthid-score`) in modo che il **servizio core pubblicato** possa valutare
le immagini prima/dopo la pulizia senza includere il codice upstream
non commerciale. Punta `wr-core` verso di esso e condividi una chiave bearer (vedi `.env.example`):```bash
# .env
WATERMARKS_SYNTHID_SCORER_URL=http://wr-synthid-score:8766
WATERMARKS_SYNTHID_SCORER_API_KEY=change-me
docker compose --profile heavy up -d
Poi POST /clean con {"options": {"detect_before": true, "detect_after": true}} restituisce synthid_before / synthid_after nel
report, e POST /detect su un'immagine restituisce il punteggio SynthID. Fail-soft:
se il sidecar è inattivo o non configurato, i report contengono
{"available": false, "error": ...} e la pulizia riesce comunque.
Il punteggio V4 utilizza artifacts/spectral_codebook_v4.npz dal checkout upstream
(`220 MB). Questa è solo rilevazione/punteggio — non rimuove i watermark
a livello di pixel.
Rimozione opzionale dei pixel CtrlRegen
Per i watermark delle immagini nel dominio dei pixel (classe SynthID, StegaStamp, Tree-Ring,
StableSignature), un backend esterno opzionale esegue la pipeline CtrlRegen
(rigenerazione controllabile ControlNet + DINOv2 IP-Adapter). Il backend è
mertizci/noai-watermark, una
reimplementazione mantenuta del metodo ICLR 2025
CtrlRegen con tiling automatico.
Il backend non è incluso e non fornisce alcun file LICENSE, quindi è trattato come
tutti i diritti riservati: viene clonato a un commit fissato e caricato a runtime.
Le sue dipendenze fissate dell'era della ricerca (requirements-ctrlregen.txt — ad es.
transformers==4.37.2, diffusers==0.27.2) contengono avvisi pubblicati e
non sono intenzionalmente aggiornate, quindi vengono installate solo all'interno del
venv dedicato che questo script crea e mai nell'immagine principale del servizio;
setup_ctrlregen.sh verifica nuovamente il commit fissato anche sui checkout
esistenti, non solo sulle clonazioni fresche.
Bootstrap```bash
SCRIPTS=service/scripts
Clones upstream (pinned commit), creates a venv, installs torch + deps.
"$SCRIPTS/setup_ctrlregen.sh"
Standalone removal (default checkout: ~/noai-watermark).
NOAI_WATERMARK_DIR=~/noai-watermark
~/noai-watermark/.venv/bin/python "$SCRIPTS/clean_ctrlregen.py" shot.png -o shot.ctrlregen.png
Su Windows usare `setup_ctrlregen.ps1` (stessi flag di `-Dir`, `-Ref`, `-Python`);
il venv finisce in `.venv\Scripts\`, che `clean_image.py` risolve già.
Sonda gli indici pubblicati dei wheel PyTorch e sceglie il più alto pari o
inferiore alla versione CUDA stampata da `nvidia-smi` che esiste realmente — quel numero
è il massimo supportato dal *driver*, e i driver sono retrocompatibili, quindi un
driver che riporta 13.1 (nessun `cu131` pubblicato) installa `cu130`. Sotto la compute
capability 7.5 forza `cu126`, l'ultimo indice i cui wheel contengono ancora
kernel Maxwell/Pascal/Volta. Installa `torch` **e** `torchvision`
insieme da quell'indice, così l'installazione delle dipendenze non può sostituirli con build
CPU da PyPI, poi verifica dopo l'installazione che `torch.cuda.is_available()`
sia true — se è stata rilevata una GPU ma torch risulta solo CPU, lo script avvisa
in modo evidente ed esce con codice diverso da zero invece di far finta che il setup sia riuscito.
### Da `clean_image.py````bash
NOAI_WATERMARK_DIR=~/noai-watermark \
~/noai-watermark/.venv/bin/python "$SCRIPTS/clean_image.py" shot.png \
-o shot.cleaned.png --remove-pixel ctrlregen
Ordine delle operazioni: prima la rimozione dei metadati, poi la rimozione dei pixel CtrlRegen, quindi
un punteggio reverse-SynthID opzionale prima/dopo (quando è impostata anche REVERSE_SYNTHID_DIR).
L'intensità è conservativa per impostazione predefinita (--ctrlregen-intensity 0.25), perché
un'intensità maggiore rimuove più watermark ma rigenera di più l'immagine.
Preset documentati: 0.15 minimale / 0.25 predefinito / 0.35 bilanciato /
0.5 aggressivo / 0.7 massimo (il valore predefinito del backend è 0.5). --ctrlregen-steps
è impostato per impostazione predefinita a 50 (passi di denoising effettivi ≈ steps × intensity).
Dimensione immagine (limite nativo 512×512)
CtrlRegen è un ControlNet di Stable Diffusion 1.5 a 512×512. Il backend risolve questo per input arbitrari, quindi qui non è esposta alcuna tiling aggiuntiva:
- ≤512 px: passaggio singolo — center-crop/resize a 512, rigenerazione, resize indietro.
- >512 px: tiling automatico con sovrapposizione (tile da 512 px, sovrapposizione di 192 px), larghezza/altezza allineate a multipli di 8, poi giunzioni con blend coseno.
- In entrambi i casi: l'output viene ridimensionato alla dimensione originale e il colore viene adattato all'immagine originale.
Immagini molto grandi (ad es. 4K) producono molti tile, quindi le esecuzioni scalano con il numero di tile (più lente e con maggiore utilizzo di VRAM). Ridimensiona preventivamente gli input di grandi dimensioni quando è pratico; dimensione dei tile e sovrapposizione sono hardcoded a monte e non sono esposte come flag.
Calcolo, modelli gated e verifica
Aspettati ~10 GB di download di modelli; una GPU è fortemente consigliata e le esecuzioni su CPU
sono lente. Alcuni modelli a monte sono gated, quindi esporta HF_TOKEN (solo env —
mai argv). clean_ctrlregen.py rifiuta di installare automaticamente le dipendenze; esegui
prima setup_ctrlregen.sh.
Non esiste un rilevatore locale per StegaStamp/Tree-Ring/StableSignature, quindi l'unico
segnale locale è il punteggio reverse-SynthID (un surrogato). Quando disponibile,
clean_image.py --remove-pixel ctrlregen riporta quel punteggio prima/dopo; il
controllo ufficiale di Google SynthID rimane l'autorità finale.
Docker```bash
make docker-ctrlregen-build
docker run --rm -e HF_TOKEN="$HF_TOKEN"
--user "$(id -u):$(id -g)"
-v "$(pwd):/data"
watermarks-remover-ctrlregen /data/shot.png -o /data/shot.ctrlregen.png
## Verifica opzionale della filigrana testuale MarkLLM
Per **esperimenti controllati**, un harness esterno opzionale avvolge
[`THU-BPM/MarkLLM`](https://github.com/THU-BPM/MarkLLM) (Apache-2.0) per
applicare una filigrana al testo di test e rilevarla nuovamente dopo una riscrittura di Layer B — ad esempio per dimostrare che
un marchio KGW (Kirchenbauer, la tua riga "open-LLM") o SynthID-Text (riga Gemini) scompare sotto la tua riscrittura. È un **harness di verifica, non un oracolo**:
il rilevamento di MarkLLM è valido solo rispetto alla *stessa* configurazione di schema + chiavi usate in
generazione, e non può certificare che un rilevatore di un fornitore fallirà.
Il backend **non è incluso**. `setup_markllm.sh` clona l'upstream a un commit
fissato, crea un venv e installa dipendenze fissate (torch + transformers); il
modello di scoring (predefinito `facebook/opt-1.3b`, Apache-2.0) viene scaricato da Hugging
Face alla prima esecuzione.```bash
SCRIPTS=service/scripts
# Bootstrap (clones upstream, creates ~/MarkLLM/.venv, installs deps).
"$SCRIPTS/setup_markllm.sh"
# Generate watermarked + unwatermarked sample text under the KGW scheme.
MARKLLM_DIR=~/MarkLLM \
~/MarkLLM/.venv/bin/python "$SCRIPTS/detect_text_watermark.py" watermark prompt.txt \
--scheme kgw -o wm.txt -o2 plain.txt
# Detect the scheme mark in a text file.
MARKLLM_DIR=~/MarkLLM \
~/MarkLLM/.venv/bin/python "$SCRIPTS/detect_text_watermark.py" detect wm.txt --scheme kgw --json
Verifica attorno a una riscrittura di Livello B: passa --markllm-scheme a
rewrite_text.py (con --markllm-dir), e registra il rilevamento MarkLLM
prima/dopo più un flag cleared:```bash
export WATERMARKS_REWRITE_BACKEND=ollama WATERMARKS_REWRITE_MODEL=llama3.2
MARKLLM_DIR=~/MarkLLM
python3 "$SCRIPTS/rewrite_text.py" wm.txt -o wm.rewritten.txt
--markllm-scheme kgw --markllm-dir "$HOME/MarkLLM" --json-stats
**Riscrittura iterativa guidata dal rilevamento:** Il Livello B ora riscrive in modo iterativo e
si ferma non appena un tentativo supera la valutazione. Ogni ciclo di valutazione genera
`--candidates` varianti (predefinito **1**, `WATERMARKS_REWRITE_CANDIDATES`)
e `--max-loops` limita quanti cicli vengono eseguiti prima che venga restituita la variante
migliore (predefinito **1**, `WATERMARKS_REWRITE_LOOPS`). Ogni variante è una
chiamata di riscrittura più una valutazione, e un ciclo termina anticipatamente al primo tentativo
che il valutatore segnala come non watermarked — quindi aumentare `--max-loops` ritenta
nuove varianti finché una valutazione non passa (una tipica riscrittura pulita costa un
tentativo). Il valutatore viene scelto per priorità:
1. **MarkLLM** — rilevamento di ricerca con la stessa configurazione, quando viene
passato `--markllm-scheme` (con `--markllm-dir`). Uno slot per rilevatore di terze parti è riservato sopra
MarkLLM per il rilevatore SynthID-text di Google, che Google ha ritirato dalla sua API
in Aug 2026 — un futuro endpoint di terze parti può essere collegato lì.
2. **divergenza lessicale bigram-Jaccard** — quando nessun rilevatore è configurato; nessun
verdetto di superamento/fallimento, quindi ogni tentativo viene generato e viene selezionato quello con
la maggiore divergenza lessicale (il comportamento originale).
`--json-stats` riporta il valutatore, i tentativi effettuati, superamento/fallimento e i record
per tentativo:```json
{
"evaluator": "markllm",
"candidates": 1,
"max_loops": 2,
"attempts_made": 2,
"passed": true,
"candidate_scores": [
{
"lexical_divergence": 0.91,
"selection_score": 0.91,
"selected": false,
"passed": false,
"evaluation": {"detector": "markllm", "available": true, "scheme": "kgw",
"is_watermarked": true, "score": 4.3, "threshold": 3.0}
},
{
"lexical_divergence": 0.84,
"selection_score": 0.84,
"selected": true,
"passed": true,
"evaluation": {"detector": "markllm", "available": true, "scheme": "kgw",
"is_watermarked": false, "score": 1.7, "threshold": 3.0}
}
],
"markllm": {"scheme": "kgw", "before": {"...": "..."}, "after": {"...": "..."},
"cleared": true, "note": "same-config only"}
}
Un rilevatore non configurato, che va in timeout o genera errori produce una
voce "available": false con una motivazione error e non fa mai fallire la
riscrittura — quel tentativo semplicemente non può passare, e il ciclo
ricade sulla selezione per divergenza lessicale. Quando il massimo è esaurito
senza un passaggio, il tentativo meno watermarkato (punteggio più basso) viene
restituito come best-effort con una nota.
Se il backend non è configurato o le sue dipendenze sono mancanti, la riscrittura procede e il report segnala che la verifica non era disponibile. Una GPU è consigliata; le esecuzioni su CPU funzionano ma sono lente, e il download del modello è di qualche GB.
Parametri di hardening:
--offlinesull'adapter (o su qualsiasi esecuzione MarkLLM) carica il modello di scoring solo dalla cache di Hugging Face — zero traffico di rete in uscita; fallisce rapidamente se non è in cache. Il codice remoto personalizzato non viene mai eseguito (iltrust_remote_codedi transformers non è mai abilitato).WATERMARKS_MARKLLM_RLIMIT_AS=<bytes>(env, POSIX) applica un limite di spazio degli indirizzi al sottoprocesso del rilevatore MarkLLM. Disattivato per impostazione predefinita perché torch/CUDA di solito necessita di ampi spazi degli indirizzi.- I file di configurazione sono limitati a 1 MiB; il checkout upstream e l'immagine di base sono fissati tramite SHA/digest.
Docker```bash
make docker-markllm-build
docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/data"
watermarks-remover-markllm detect /data/wm.txt --scheme kgw --json
### Verifica con la stessa chiave di Keyed-Gumbel (Aaronson EXP)
[Il rapporto tecnico di ARBI](https://arbicity.com/news/ai-text-watermarking-for-self-hosted-ai/) descrive
il watermark testuale keyed-Gumbel ("esponenziale") — ora distribuito nel motore
open-source arbi-serve (`ARBI_WATERMARK_KEY`) — in cui il rumore del sampler è derivato
da un hash con chiave della finestra di contesto degli ultimi 4 token. Il rilevamento è un
**replay model-free**: ricalcola `u = PRF(Hash(key, window), token)` dal
solo testo e verifica la coda Gamma, quindi non richiede GPU, modello o logits.
Questo repository distribuisce quel rilevatore come `detect_gumbel.py` (solo stdlib; il p-value
è l'identità esatta della somma di Poisson per una forma Gamma intera):```bash
# Text mode (deterministic word/run tokenizer) — quick checks and rewrite-loop
# evaluation; exact replay against a real engine needs its tokenizer:
python3 service/scripts/detect_gumbel.py draft.txt --key 0x... --json
# Exact replay: pass the engine's token ids (JSON array or one per line).
python3 service/scripts/detect_gumbel.py ids.json --tokens --key 0x... --json
Stesso caveat di onestà di MarkLLM: questa è una riproduzione con la stessa chiave — valida solo contro la stessa chiave, tokenizer e layout PRF usati in generazione, e un risultato negativo non dimostra nulla. Il layout HMAC-SHA256 qui è un'istanza verificabile, non bit-compatibile con alcun kernel di motore specifico (vedi la docstring del modulo per cosa adattare per una riproduzione esatta).
Riscrittura guidata dal rilevamento: passa --gumbel-key a rewrite_text.py
(env: WATERMARKS_GUMBEL_KEY, preferito) e il ciclo di riscrittura iterativo è
guidato dalla riproduzione Gumbel con la stessa chiave — la priorità del valutatore diventa gumbel >
MarkLLM > divergenza lessicale — con un report gumbel.before/after/cleared:```bash
export WATERMARKS_REWRITE_BACKEND=ollama WATERMARKS_REWRITE_MODEL=llama3.2
export WATERMARKS_GUMBEL_KEY=0x...
python3 "$SCRIPTS/rewrite_text.py" wm.txt -o wm.rewritten.txt --json-stats
La chiave non appare mai nelle statistiche o nei log. Gli operatori self-hosted che possiedono la chiave del proprio motore possono verificare che una riscrittura abbia rimosso un marchio Gumbel; tutti gli altri considerano il Livello B come puramente best-effort.
## Benchmark opzionale per la rimozione di SynthID-text
[`bench_synthid_text.py`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/service/scripts/bench_synthid_text.py) misura quanto
efficacemente una riscrittura di Livello B rimuove i watermark della classe
SynthID-text e a quale costo. Genera campioni con e senza watermark tramite lo
schema SynthID di MarkLLM (rilevamento con la stessa configurazione, con
controllo di sanità), esegue le tue varianti di riscrittura (tattica × numero
massimo di tentativi di riscrittura; il ciclo si interrompe anticipatamente al
superamento) più i controlli (nessuna rimozione, solo Livello A, verifica
opzionale di ri-marcatura), e scrive un `report.md` /
`results.json` / `results.csv` condivisibile. Guida completa:
[`docs/synthid-text-benchmark.md`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/docs/synthid-text-benchmark.md).
Richiede un checkout di MarkLLM (`setup_markllm.sh` / `MARKLLM_DIR`) e un
backend di riscrittura. **Il modello di riscrittura è un LLM che configuri tu** — lo stesso
backend `rewrite_text.py` utilizzato dalla skill. Il modello predefinito di MarkLLM
`facebook/opt-1.3b` (`--markllm-model`) è solo il generatore/rilevatore di
watermark; non riscrive mai. Configura il modello di riscrittura tramite variabili
d'ambiente o flag del benchmark (rispecchiano la
[tabella di configurazione](#configuration-env-vars-for-docker-compose) sopra):
| Variabile d'ambiente | Flag del benchmark | Predefinito | Significato |
| --- | --- | --- | --- |
| `WATERMARKS_REWRITE_BACKEND` | `--rewrite-backend` | `ollama` | `ollama` o `openai-compatible` |
| `WATERMARKS_REWRITE_MODEL` | `--rewrite-model` | *(obbligatorio)* | L'LLM che esegue la riscrittura (es. `llama3.2`, `deepseek-v4-flash`) |
| `WATERMARKS_REWRITE_BASE_URL` | `--rewrite-base-url` | `http://127.0.0.1:11434` | Endpoint; il valore predefinito di Ollama è loopback |
| `WATERMARKS_REWRITE_API_KEY` | `--rewrite-api-key` | — | Chiave API (solo tramite env nel processo figlio, mai argv) |
| `WATERMARKS_REWRITE_ALLOW_REMOTE=1` | `--rewrite-allow-remote` | disattivato | Necessario per inviare contenuti a endpoint non loopback |```bash
# Ollama (loopback):
python3 service/scripts/bench_synthid_text.py --markllm-dir ~/MarkLLM \
--rewrite-backend ollama --rewrite-model llama3.2
# OpenAI-compatible API (remote):
WATERMARKS_REWRITE_API_KEY=... python3 service/scripts/bench_synthid_text.py \
--markllm-dir ~/MarkLLM --rewrite-backend openai-compatible \
--rewrite-model deepseek-v4-flash --rewrite-base-url https://api.deepseek.com \
--rewrite-allow-remote
Usa un modello non-origin per la riscrittura (non riscrivere con lo stesso
modello watermarked che ha generato il testo) altrimenti la riscrittura può ri-applicare il
marchio sull'output; --restamp-control misura questo.
Harness opzionale per watermark su immagini MarkDiffusion
Per esperimenti controllati sulle immagini, un harness esterno opzionale avvolge
THU-BPM/MarkDiffusion (Apache-2.0),
un toolkit di watermarking generativo per modelli di diffusione latente (incorpora i marchi
— non li rimuove). Lo usiamo per tre cose:
- Harness di verifica (come MarkLLM, ma per le immagini): applica un watermark a un'immagine di test con uno schema, esegui la rimozione e ri-rileva con la stessa configurazione di schema — ad esempio dimostra che un marchio di classe Tree-Ring viene eliminato dalla tua pipeline. È un harness di verifica, non un oracolo: il rilevamento richiede il modello generatore (e le chiavi per gli schemi basati su chiave), quindi non può certificare che un rilevatore di un fornitore fallirà su un'immagine arbitraria.
- Motore opzionale di rimozione a livello di pixel: il suo attacco di rigenerazione
DiffusionPurificationè esposto comeclean_image.py --remove-pixel diffusion, un'alternativa a CtrlRegen. È rigenerazione blind (nessun condizionamento ControlNet), quindi deriva il contenuto dell'immagine più di CtrlRegen — intensità predefinita conservativa (0.3), trattato come fallback/confronto, mai come garanzia. - Rilevatore locale con lo stesso schema per marchi di classe Tree-Ring, colmando parzialmente la lacuna "nessun rilevatore locale per StegaStamp/Tree-Ring/StableSignature" (copre Tree-Ring/Ring-ID/Gaussian-Shading ecc., non StegaStamp / StableSignature / SynthID-media).
Il backend non è incluso. setup_markdiffusion.sh crea un venv e
installa markdiffusion==1.0.2 da PyPI (versione bloccata), con torch installato dall'indice
della piattaforma corretta; --checkout installa invece un clone editable a un commit bloccato.
Il modello Stable Diffusion (predefinito
huanzi05/stable-diffusion-2-1-base) viene scaricato da Hugging Face alla prima esecuzione.```bash
SCRIPTS=service/scripts
Bootstrap (PyPI pin default; creates ~/markdiffusion/.venv, installs deps).
"$SCRIPTS/setup_markdiffusion.sh"
1. Generate a Tree-Ring watermarked image (+ unwatermarked control).
echo "a red fox in snow" > /tmp/prompt.txt
MARKDIFFUSION_DIR=~/markdiffusion
~/markdiffusion/.venv/bin/python "$SCRIPTS/markdiffusion_harness.py" watermark
/tmp/prompt.txt -o wm.png -o2 plain.png --scheme tr --json
2. Remove with the DiffusionPurification regeneration attack.
MARKDIFFUSION_DIR=~/markdiffusion
~/markdiffusion/.venv/bin/python "$SCRIPTS/markdiffusion_harness.py" purify
wm.png -o wm.purified.png --purification-intensity 0.3 --json
3. Re-detect with the SAME scheme config.
MARKDIFFUSION_DIR=~/markdiffusion
~/markdiffusion/.venv/bin/python "$SCRIPTS/markdiffusion_harness.py" detect
wm.purified.png --scheme tr --detector-type l1_distance --json
Oppure esegui la purificazione come parte della normale pipeline delle immagini:```bash
MARKDIFFUSION_DIR=~/markdiffusion \
~/markdiffusion/.venv/bin/python "$SCRIPTS/clean_image.py" shot.png \
-o shot.cleaned.png --remove-pixel diffusion
Le opzioni di hardening rispecchiano l'harness MarkLLM: --offline carica il modello solo dalla cache di Hugging Face (zero traffico di rete in uscita, nessun codice remoto), HF_TOKEN è solo tramite variabile d'ambiente (mai argv), le configurazioni degli algoritmi sono limitate a 1 MiB, e il sottoprocesso riceve gli stessi limiti di risorse più elevati di CtrlRegen.
Docker```bash
make docker-markdiffusion-build
docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/data"
watermarks-remover-markdiffusion detect /data/wm.png --scheme tr --json
L'immagine installa una CPU torch; gli utenti CUDA dovrebbero eseguire `setup_markdiffusion.sh`
sull'host. I download dei modelli passano comunque dall'HF hub alla prima esecuzione.
## Matrice di copertura
| Canale | Claude | Gemini/SynthID | OpenAI | Open-LLM |
| --- | --- | --- | --- | --- |
| Testo Unicode / basato su modifiche | Layer A | Layer A | Layer A | Layer A |
| **Testo con campionamento statistico** | Layer B best-effort (seam Claude quando l'API di rilevamento di Anthropic sarà disponibile) | Layer B best-effort (+ harness MarkLLM stessa-config; Google ha ritirato il rilevatore del fornitore ad agosto 2026) | Layer B se presente | Layer B best-effort + harness MarkLLM opzionale |
| C2PA / metadati file | Sì (formati elencati) | Sì quando presente | Sì quando presente | Sì quando presente |
| Marchi su immagini a pixel | Fuori ambito | Punteggio SynthID opzionale + rimozione CtrlRegen (esterno); rilevamento MarkDiffusion stesso-schema opzionale + rimozione DiffusionPurification (esterno) | Fuori ambito | Rimozione CtrlRegen / MarkDiffusion opzionale (esterno) |
| Backdoor di addestramento | Fuori ambito | Fuori ambito | Fuori ambito | Fuori ambito |
Dettagli: [`skills/remove-ai-marks/references/vendor-notes.md`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/skills/remove-ai-marks/references/vendor-notes.md), [`mark-classes.md`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/skills/remove-ai-marks/references/mark-classes.md).
---
## Come funziona la marcatura del testo (breve)
I watermark moderni degli LLM spesso nascondono un segnale in **quali token vengono scelti** (bias generativo / di campionamento), non solo in caratteri invisibili. Gli schemi basati su modifiche iniettano Unicode o regole di sinonimi. Gli schemi su file allegano **C2PA** o metadati del generatore.
- **Layer A** rimuove i carrier Unicode basati su modifiche (testabile).
- **Layer B** attacca i watermark di campionamento tramite riscrittura pesante (best-effort; attacchi standard della letteratura come parafrasi / retro-traduzione).
- **I cleaner di file** rimuovono C2PA/XMP/props dai container supportati.
Finché i fornitori non rilasciano rilevatori e chiavi pubblici, **nessuno strumento può certificare onestamente** "questo fallisce il controllo ufficiale". I report devono separare il lavoro verificabile da quello best-effort.
Preferisci un modello **non-origin** per il Layer B (non riscrivere testo Claude con Claude se stai cercando di evitare un nuovo stampaggio).
---
## Disclaimer: quanto costa rimuovere un watermark dal testo
I watermark del testo vivono **nella formulazione stessa**: il segnale è distribuito tra le scelte dei token, quindi quasi ogni frase ne porta un po'. Ne derivano due conseguenze, ed è per questo che il Layer B è onestamente descritto come *best-effort* anziché come una gomma magica.
1. **Rimozione significa riformulare, non ristrutturare.** Spostare paragrafi, cambiare intestazioni o fare ritocchi leggeri sposta appena il segnale. Eliminare un marchio statistico richiede di riscrivere una frazione sostanziale del testo — frase per frase, non sezione per sezione.
2. **La riformulazione degrada la copia.** Qualsiasi riscrittura sostituisce le scelte di parole originali con quelle del modello che riscrive, appiattendo tono, voce e precisione. Su testi di produzione (SEO, marketing, lavoro per clienti) quella degradazione è reale e spesso visibile alle persone a cui sta più a cuore la scrittura. È come prendere un testo da un modello di fascia alta e chiedere a un modello meno capace di riscriverlo da zero: il risultato non può superare il tetto del modello che riscrive.
Il che porta all'onesta domanda che chiude il cerchio:
> Se il piano è comunque riscrivere il testo con un modello più economico, perché pagare per un modello premium fin dall'inizio? Generare direttamente con il modello più economico è più semplice, più economico e produce lo stesso — o migliore — risultato finale.
Il Layer B ha senso quando vuoi specificamente il **pensiero e la stesura** del modello premium e accetti un passaggio di riscrittura per soddisfare un requisito di igiene o privacy — non come via economica per ottenere testo senza marchi.
**Quando saltare il Layer B:**
- **La qualità conta più dell'igiene:** usa il percorso lossless — la pulizia Unicode del Layer A più i cleaner dei metadati dei file — e mantieni la prosa originale.
- **Riscrivi comunque:** usa un modello **non-origin** (riscrivere con il modello di origine può ri-stampare il testo), e ricorda che il rischio residuo rimane — nessuno strumento può certificare che un rilevatore del fornitore fallirà.
---
## Formati di file
| Formato | Ispeziona | Pulisci |
| --- | --- | --- |
| PNG / JPEG / WebP | chunk C2PA / APP11 / RIFF `C2PA`, indizi XMP AI | Elimina i segmenti di metadati |
| AVIF / HEIC | box ISOBMFF `jumb` / XMP `uuid` | Elimina i box |
| BMP | Byte finali non-immagine (nessun canale standardizzato) | Tronca i metadati finali, correggi il campo dimensione file |
| GIF | Estensioni applicative Comment / XMP | Elimina commento e XMP, mantieni il loop `NETSCAPE2.0` |
| TIFF (classico + BigTIFF) | Tag IFD: XMP, EXIF, GPS, IPTC, MakerNote | Elimina i tag, azzera i payload, mantieni le strip |
| SVG | `<metadata>`, XMP | Rimuovi i blocchi |
| PDF | Byte/XMP + strumenti opzionali | **exiftool** poi **qpdf**, poi **ghostscript** per i metadati dentro le immagini incorporate; ogni strumento mancante degrada un livello diverso (strip del documento, riscrittura strutturale, immagini incorporate) |
| DOCX | docProps / customXml | Pulisci le props, elimina customXml |
| EPUB | Metadati OPF, meta/JSON-LD XHTML, media incorporati | Pulisci OPF, rimuovi meta XHTML, pulisci media + Layer A (salta le parti cifrate) |
| ODT | meta.xml | Elimina metadati del generatore / simil-AI |
| HTML | meta, JSON-LD, data-ai* | Rimuovi tag/attr |
| Markdown | Chiavi AI nel frontmatter YAML | Elimina le chiavi + corpo Layer A |
| MP4 / MOV / M4A / M4V | box ISOBMFF `jumb`/`uuid` (stesso meccanismo di AVIF/HEIC) + tag del generatore `moov/udta` | Elimina i box |
| WAV | chunk RIFF `C2PA` / `LIST INFO`, chunk `id3\x20` incorporato | Elimina i chunk |
| MP3 | frame ID3v2 (v2.3/v2.4 per-frame; v2.2 intero-tag) | Elimina i frame corrispondenti o l'intero tag |
| FLAC | Manifest C2PA in un frame ID3v2 `GEOB` | Elimina il frame corrispondente o l'intero tag ID3v2 |
Il supporto FLAC copre il carrier ID3v2 standardizzato di C2PA. I blocchi di metadati FLAC nativi,
i Vorbis Comments e i watermark nel dominio della forma d'onda vengono lasciati intatti.
#### Perché il PDF ha bisogno di qpdf, non solo di exiftool
ExifTool scrive i PDF **in modo incrementale**. `exiftool -all=` aggiunge un
blocco `%BeginExifToolUpdate` che libera l'oggetto Info e rimuove `/Info` dal
trailer — ma i byte dei metadati originali restano nel file alla lettera, e
exiftool stesso può annullare la modifica con `-PDF-update:all=`. Il comando esce
con `0`, i visualizzatori non mostrano metadati, e il file diventa *più grande*, che è l'indizio.
Per uno strumento che rimuove la provenienza questa è una fuga silenziosa, quindi `clean_pdf` segue
il passaggio di exiftool con `qpdf --linearize`, che ri-serializza il documento
dal suo grafo di oggetti ed elimina gli oggetti ora non referenziati. Senza `qpdf`
installato la pulizia viene comunque eseguita, ma lo dichiara:```
warning: exiftool PDF edits are incremental — the original metadata bytes
remain recoverable; install qpdf for a structural rewrite
Perché qpdf non basta per le immagini all'interno del PDF
Entrambe le passate precedenti operano sul documento: il dizionario Info, il pacchetto
XMP, il grafo degli oggetti. Nessuna delle due scende in un XObject immagine, quindi una scansione o un
export di Photoshop — una pagina che è un unico grande JPEG — conserva tutto ciò che l'immagine
porta con sé. Su un PDF reale esportato da Photoshop questo lascia 27 tag al loro posto dopo una
pulizia "riuscita", tra cui IFD0:Software, i timestamp di acquisizione e una miniatura
di anteprima; anche un manifest C2PA allegato alla stessa immagine sopravvive.
Quindi clean_pdf aggiunge una terza passata, deep_images, guidata dal pdfwrite di
Ghostscript. Viene eseguita in due livelli e si ferma non appena il file è pulito:
- Lossless.
pdfwritecon pass-through ricostruisce il documento dal grafo degli oggetti copiando i dati compressi dell'immagine byte per byte — verificato tramite hashing degli stream prima e dopo. Questo elimina tutto ciò che il PDF avvolgeva attorno all'immagine. Il pass-through copre i codec che Ghostscript supporta per esso, JPEG (DCTDecode) e JPEG2000 (JPXDecode); le immagini Flate, CCITT e LZW vengono decodificate e ricodificate, il che è lossless in pratica per quei codec ma non identico byte per byte.neverè l'opzione per un documento i cui stream devono sopravvivere intatti. - Ricodifica, solo su evidenza. Qualsiasi cosa risieda nei segmenti APPn del JPEG stesso — EXIF in APP1, un manifest C2PA in APP11, risorse Photoshop in
APP13 — viaggia con i byte a cui è allegata, quindi il pass-through la preserva. Il livello 2 esegue la stessa passata con il pass-through disattivato, e solo quando il livello 1
ha dimostrato di aver lasciato qualcosa indietro: un marcatore AI/C2PA in qualsiasi modalità, o, con
always, qualsiasi metadato APPn sopravvissuto. APP0 (JFIF) e APP2 (ICC) vengono lasciati intatti — il primo è strutturale e il secondo decide come vengono letti i colori. I pixel vengono spesi su evidenza, mai su sospetto.
deep_images accetta auto (predefinito: solo il livello 1 quando i marcatori sono sopravvissuti alla
rimozione dal documento, poi il livello 2 se sopravvivono anche a quella), always (livello 1 per ogni
PDF, con escalation al livello 2 anche per EXIF di fotocamere ed editor), lossless (solo livello 1
— mai ricomprimere, e segnalare tutto ciò che sopravvive tramite i soliti
campi still_has_c2pa / post_findings) e never. Un valore non riconosciuto viene
rifiutato anziché essere silenziosamente trattato come auto. Il report indica quali livelli sono stati eseguiti
tramite meta.deep_image_pass e meta.images_reencoded, e quando la passata viene
saltata nomina l'opzione che andrebbe oltre:```text
deep image pass not needed for AI/C2PA markers; pass deep_images="always"
to also clear non-AI EXIF inside images
Senza Ghostscript installato la pulizia viene comunque eseguita e indica ciò che non ha potuto
raggiungere:```text
warning: metadata inside embedded images left in place; install ghostscript
for the deep image pass
La rimozione del watermark nel dominio dei pixel è ora disponibile come backend CtrlRegen esterno opzionale (vedi sopra); è un remover rigenerativo, non una garanzia. Il soft binding C2PA (watermark nel contenuto che può ricollegare un manifest Content Credentials remoto dopo che i metadati sono stati rimossi) rimane fuori ambito. La rimozione del C2PA hard-bound non cancella quei canali.
Rischio residuo dopo una pulizia
Questo strumento segnala rimozioni verificabili (conteggi Unicode, azioni sui metadati) e riscritture Layer B best-effort. Non può certificare che i rilevatori dei vendor falliranno.
Per verificare tu stesso i segnali residui (opzionale, esterno):
| Canale | Cosa rimuoviamo | Cosa può rimanere | Verifica esterna (esempi) |
|---|---|---|---|
| C2PA hard-bound / EXIF / XMP | Sì | Marchi soft-bound / nei pixel | c2patool, Content Credentials verify |
| Media classe SynthID | Rimozione opzionale dei pixel (CtrlRegen esterno); altrimenti punteggio locale | Watermark audio/video; watermark residuo nei pixel dopo la rimozione | Strumenti del provider (es. Google SynthID / rilevatore Vertex dove offerto); scorer locale opzionale reverse-SynthID |
| Testo statistico | Riscrittura best-effort | Marchi forti dopo modifiche leggere | Nessun rilevatore universale pubblico; strumenti dei vendor quando disponibili |
Contesto industriale a due livelli (C2PA + watermark impercettibile): guida dell'Institute of AI PM.
Rilevatori di watermark
Checker forniti dai vendor per verificare se il contenuto porta marchi di provenienza AI:
- Claude: Check if a file was made with Claude — legge le credenziali di contenuto C2PA in immagini, video e audio per segnalare se Claude è stato coinvolto nella produzione del file; funziona nel browser. L'API di rilevamento watermark testuale di Claude è attualmente in anteprima privata.
- OpenAI: Verify OpenAI-generated content — carica un'immagine o un file audio e verifica la presenza di segnali di provenienza OpenAI (metadati C2PA e watermark SynthID). È disponibile anche un'API programmatica.
- Google DeepMind: SynthID — la tecnologia di watermarking di Google per immagini, audio, testo e video generati dall'AI, con una panoramica di come i marchi impercettibili vengono incorporati e rilevati.
- Gemini: Verify AI-generated images, videos, and audio — la guida di Google per verificare i file nell'app Gemini utilizzando i watermark SynthID e le Content Credentials, inclusi i limiti di caricamento e come leggere i risultati.
Opzioni di rimozione (riepilogo)
| Opzione | Rimuove | Note |
|---|---|---|
| Unicode scrub (Layer A) | ZWSP, bidi, tag, spazi esotici, … | Default sicuro per il testo |
| Rewrite (Layer B) | Marchi statistici sui token (best-effort) | Sempre offerto dalla skill; ha un costo in stile — vedi Disclaimer |
| Container/metadata strip | Provenienza del file | Vedi tabella dei formati |
| Rimozione pixel CtrlRegen (opzionale) | Marchi immagine nel dominio dei pixel (classe SynthID, StegaStamp, Tree-Ring, StableSignature) | Backend esterno; calcolo pesante; intensità predefinita conservativa |
| Rimozione pixel DiffusionPurification (opzionale) | Marchi immagine nel dominio dei pixel (classe Tree-Ring) | Backend MarkDiffusion; rigenerazione cieca (più deriva rispetto a CtrlRegen); intensità predefinita conservativa |
| Modelli locali open-weight | Evitare di ri-marchiare con il modello di origine | Alternativa operativa |
Matrice: skills/remove-ai-marks/references/removal-matrix.md.
Etica e disclaimer
Vedi skills/remove-ai-marks/references/ethics.md. Per privacy e ricerca sul tuo contenuto — non frode accademica o false dichiarazioni di "scritto da umano".
Uso responsabile: Questo progetto è destinato a contenuti di tua proprietà o che sei autorizzato a elaborare. Gli utenti devono rispettare le normative locali e utilizzarlo in modo responsabile. Gli sviluppatori declinano ogni responsabilità per potenziali usi impropri da parte degli utenti.
Ecosistema
Progetti di terze parti che wrappano o completano questo repository, elencati solo per facilità di scoperta. Non sono mantenuti, approvati o supportati da questo progetto. Questo progetto non revisiona il loro codice, non garantisce il loro comportamento o le loro garanzie, e non si assume alcuna responsabilità per qualsiasi cosa tu installi o esegua da questo elenco. Ogni progetto è regolato dalla propria licenza, dai propri maintainer e dalla propria documentazione — leggili prima di utilizzarlo.
MetaClean — GUI desktop
MetaClean è un'applicazione desktop indipendente Rust/Tauri con licenza MIT (Windows, macOS, Linux) che fornisce una GUI nativa pacchettizzata per la pulizia dei metadati tramite drag-and-drop, con system tray e integrazione con Explorer. È una codebase separata: non chiama il servizio Python di questo repository, e i suoi formati supportati e le sue garanzie di pulizia differiscono da quelle di questo progetto. Vedi il suo README per i dettagli.
unmark-web — interfaccia web nel browser
unmark-web è un client web statico indipendente con licenza MIT. Rimuove i marchi Unicode invisibili dal testo e rimuove i metadati di provenienza dalle immagini interamente nel browser, e può opzionalmente chiamare il servizio HTTP di questo repository per i formati che non gestisce localmente. È una codebase separata e non è affiliata a questo progetto; vedi il suo README per ambito e limiti.
DropMarks — GUI macOS
DropMarks è un'applicazione macOS SwiftUI indipendente con licenza MIT. Chiama inspect_file.py / clean_file.py di questo repository (e opzionalmente rewrite_text.py) tramite uno snapshot vendored di quegli script stdlib. È una codebase separata e non è affiliata a questo progetto; vedi il suo README per ambito e limiti.
Aggiungere un progetto
Per registrare un progetto qui, apri una PR aggiungendo una breve voce — nome del progetto, cosa wrappa o aggiunge, e un link al suo repository. Mantieni le voci brevi e fattuali; non rivendicare compatibilità con, o approvazione da parte di, questo progetto. Un progetto elencato dovrebbe basarsi su questo repository o integrarlo — ad esempio, chiamando il suo servizio o riutilizzando il suo motore di rilevamento — piuttosto che limitarsi ad affrontare lo stesso problema in modo indipendente. Evita nomi che iniziano con o assomigliano molto a watermarks-remover — i nomi simili rendono difficile distinguere un progetto dall'altro.
Hook pre-commit
Il gating CI esiste già (esportazione SARIF di audit_dir.py, vedi il contesto della matrice di copertura) — gli hook pre-commit qui sotto intercettano la stessa classe di problemi prima, ancora prima che un file marcato venga committato. Entrambi wrappano le CLI esistenti (audit_dir.py / clean_file.py) — nessuna logica di rilevamento separata.```yaml
.pre-commit-config.yaml
repos:
- repo: https://github.com/guillaumemeyer/watermarks-remover
rev: v0.5.0 # pin to a tag/commit
hooks:
- id: watermarks-remover-check # fails the commit if marks are found
- id: watermarks-remover-clean # opt-in: cleans staged files in place instead
`watermarks-remover-check` fallisce il commit ed elenca i risultati; `watermarks-remover-clean` è opt-in e riscrive i file in stage sul posto (esce con 1 così puoi rivedere il diff e rifare lo stage — la stessa convenzione degli hook di auto-correzione come `ruff --fix`). Quando il cleaner non riesce a processare un file del tutto — è andato in crash, è stato terminato, o non ha prodotto alcun report — `watermarks-remover-clean` nomina quel file ed esce con 3, così un cleaner che ha fallito non viene mai scambiato per un file già pulito. Esegui l'uno o l'altro manualmente con `python3 service/scripts/check_staged.py <files...>` / `clean_staged.py <files...>`.
## Tests```bash
python3 -m venv .venv && .venv/bin/pip install pytest
.venv/bin/python -m pytest # or: make test
make smoke # quick CLI smoke on fixtures
Changelog
v0.7.0 — riscrittura di Layer B in /clean, modulo di watermark-stealing, rimozione watermark audio/video e ampiezza di benchmark/tooling
v0.7.0 porta la riscrittura del marchio statistico di Layer B direttamente nel servizio /clean, guidata da una strategia configurabile e ottimizzata tramite benchmark ([email protected],[email protected]). Insieme a questo: un modulo black-box di watermark-stealing, rimozione distruttiva di watermark audio e video per-frame, un benchmark di riscrittura sostanzialmente più ricco e una serie di correzioni di hardening, sicurezza e tooling.
Riscrittura di Layer B nel servizio
/cleanesegue la riscrittura di Layer B per il testo dopo Layer A. Il valore predefinito proviene daconfig/clean_strategy.json; unoptions.strategyper-richiesta lo sovrascrive, e/cleanrifiuta con 400 quando il backend richiesto non è configurato (#315). Precedenza di configurazione:--strategy-config>WATERMARKS_CLEAN_STRATEGY_FILE>config/clean_strategy.json.- Nuova tattica di riscrittura
mlm: maschera una frazione di parole di contenuto e riempie conroberta-large— una modifica locale non autoregressiva, quindi l'output mescola il flusso di token originale con le predizioni del masked-LM (#311). - La tattica
humanizeora applica deterministicamente il passaggio humanizer-skill (virgolette dritte, nessun trattino en/em, collasso dei riempitivi,utilize→use) e nomina le regole dello scrittore umano nel prompt (#311).rewrite_text.pyha guadagnato un percorso CLI--strategy. - Correttezza della riscrittura: tokenizzazione Unicode delle parole nella divergenza lessicale (#305); confronto dei margini grezzi prima dell'arrotondamento e registrazione dei metadati di selezione / p-values classificati (#249).
Benchmark
- Ricerca della ricetta SynthID + misurazione robusta (#280); rinominato il vocabolario di riscrittura, ricerca cross-input e ordinamento humanize-last (#302); raccomanda solo strategie che superano ancora il controllo dopo la rifinitura humanize (#307).
- API bulk Pangram come backend di human-likeness (#296); benchmark minimal-rewrite-level rafforzato con un corpus di 30 documenti (#257); griglia di pesi validata + ricerca della ricetta ampliata (#294); corpus di benchmark in polacco (#295).
Watermark stealing
- Nuovo modulo black-box di watermark-stealing e downloader di prompt-corpus (#303); pulizia dello stato obsoleto in caso di fallimento della sonda start-over (#310).
Audio / video / immagine
- Catena distruttiva di rimozione watermark audio per silentcipher/AudioSeal/WavMark (tempo + pitch + EQ + ri-encoding a basso bitrate → M4A) (#266).
- Purificazione video per-frame TrustMark che annulla il voto temporale (#265).
- Box
uuiddi provenienza dei contenuti C2PA riconosciuto su MP4/MOV/AVIF/HEIC (#264). - Conservazione delle code MP4 troncate durante lo stripping (#242); mantenere la destinazione del ri-encoding audio distinta da quella del container-clean (#278).
- Salta l'output exiftool scartato e il SynthID ridondante nella scansione post-clean (#261); degrada in modo pulito quando exiftool non riesce a elaborare un PDF (#281).
- Limita a 1 MiB il
zTXt/iTXtPNG decompresso (#308); rimuove le dichiarazioni DOCTYPE/ENTITY XML negli SVG (#288); mantiene i membri binari DOCX byte-safe (#314); preservaAppVersionin OOXML (#289).
Servizio HTTP e CLI
- Opzione
/cleanper mantenere spazi esotici, in linea con la CLI (#274);/inspectespone classi di evidenza esplicite nel payload sospetto (#277); timestamp nei log delle richieste HTTP (#256); passa i byte del payload al punteggio SynthID HTTP e ainspect_*per evitare una rilettura ridondante. clean_file.pyha guadagnato-q/--quiet/--only-changed(#254).
Skills, plugin e hook
- Punteggio di stilometria e leve del rilevatore per
clean-user-facing-text(#258); launcher dell'hook PostToolUse reso cross-platform (#255); l'hook pre-commit considera i file non-testuali clean byte-identici come modificati (#238).
Audit
audit_dir.pyscansiona file sorgente, documentazione e i18n che il router ha saltato (#284); scansiona.ts/.tsx/.jsx/.gde allinea la confidenza degli spazi tra i formati (#273); supportoaudit_website.py --sarif(#194); rafforza backup in-place, stato dei file clean, verdetto SynthID, ID3v2 troncato e routing zip (#201).
Sicurezza
- Rimuove ReDoS polinomiale nelle scansioni data-URI e JSON-LD (#306); blocca i redirect HTTP nello scorer SynthID per prevenire SSRF (#252).
CI, tooling e documentazione
- La CI fallisce quando i requisiti dei backend opzionali non possono essere risolti (#301); l'immagine Docker segnala ffmpeg come utilizzabile e installa Ghostscript (#272); aggiornamenti delle dipendenze (cython #299, scipy #298, ruff #297, docker/setup-buildx-action #237).
- Documentazione: sezione Watermark Detectors, riferimento al blog ETH SRI "Probing SynthID", policy Ecosystem (rimuove ClaudeWatermarks; richiede che i progetti elencati usino questo repo) (#292).
v0.6.0 — copertura di formati più ampia, hardening di Layer A, distribuzione di plugin e hook, e riscrittura guidata dal rilevamento
Copertura di formati e container
- AVIF / HEIC: metadati nativi stdlib e stripping C2PA (#84, #85)
- BMP / GIF / TIFF: rilevamento, ispezione e pulizia dei metadati con stdlib — le estensioni comment/XMP dei GIF vengono eliminate mentre il looping
NETSCAPE2.0e altri chunk di animazione vengono preservati; i metadati IFD TIFF (XMP/EXIF/GPS/IPTC/MakerNote) vengono eliminati con i payload azzerati e gli offset di strip mantenuti, sia per TIFF classico che BigTIFF; i metadati in coda BMP vengono troncati con il campo dimensione file riscritto (#107) - EPUB: pulizia del container con stdlib — metadati OPF e meta/JSON-LD XHTML ripuliti, media raster/SVG incorporati rimossi, Layer A applicato al testo del corpo XHTML, parti di metadati portatrici di marker eliminate e parti cifrate OCF lasciate intatte (#107)
- XLSX / PPTX / DOCX (OOXML): pulizia nativa stdlib di metadati del container, testo e media incorporati; svuota sempre i campi di provenienza
docPropsdei DOCX; elimina le relazioni pendenti dopo la rimozione dicustomXml; esegue Layer A sul testo del corpo DOCX/ODT; decodifica le entità XML prima della pulizia di Layer A (#91, #100, #76, #83, #73, #80, #74, #81, #142) - Container SGML/vettoriali: stripping dei metadati in tempo lineare per SVG/ODT (GHSA-7vpp-96qp-j9wh) (#147); ispeziona e pulisce ricorsivamente i data URI raster incorporati in SVG, HTML e Markdown (#87, #88)
- Audio / video: stripping dei metadati AI/C2PA per MP4/MOV, WAV e MP3 (#139); rilevamento e rimozione del chunk C2PA RIFF WAV; supporto ai metadati C2PA FLAC; rifiuta il parsing parziale dei frame ID3v2 (#232); preserva gli offset dei media MP4 durante lo stripping dei metadati (#183)
- PDF: raggiunge i metadati che risiedono all'interno delle immagini incorporate e smette di ridimensionare il PDF per rimuovere XMP; esegue il passaggio deep-image indipendentemente dall'installazione di exiftool; rispetta i byte di riempimento dei marker JPEG e condivide un unico segment walker
- PNG: rileva i nomi dei prodotti dei generatori AI nei metadati testuali PNG; rileva i marker AI nel testo PNG compresso (#127); mantiene la coda troncata invece di eliminarla negli strip png/isobmff (#182)
Hardening di Layer A (Unicode invisibile)
- Hardening consolidato di Layer A (#133): rimuove i code point riservati
Default_Ignorablesenza un uso legittimo di interscambio (U+2065,U+FFF0–U+FFF8,U+E0000,U+E0080–U+E00FF,U+E01F0–U+E0FFF— segnalati comereserved_ignorable), i 66 noncaratteri (U+FDD0–U+FDEFpiùU+FFFE/U+FFFFper piano — segnalati comenoncharacter) e tre portatori Default_Ignorable a rendering vuoto che il catch-allCfnon vedeva mai (U+180F,U+3164,U+FFA0). Ognuno ha la stessa preservazione in-contesto dei suoi fratelli già coperti, quindi il testo con sillabe parziali non viene corrotto, e ognuno è applicato sia al motore del servizio che alla copia vendored della lightweight-skill - Smetti di rimuovere i controlli di formato del layout visibile accanto al proprio script: i controlli quadrat dei geroglifici egizi (
U+13430–U+1343F), i controlli della stenografia Duployan (U+1BCA0–U+1BCA3) e i controlli musicali beam/tie/slur/phrase (U+1D173–U+1D17A) sono ora preservati quando adiacenti al proprio script e ancora rimossi (e segnalati) quando fluttuano tra testo non correlato; la modalità paranoica--strip-emoji-glueli rimuove comunque ovunque - Rifinitura emoji / script: preserva VS16 dopo emoji singleton al di fuori degli intervalli di blocco; preserva gli script joiner, le emoji bandiera e i segni Cf arabi; preserva l'Unicode multilingue durante la pulizia del testo (#34)
Riscrittura di Layer B e rilevamento watermark
- Riscrittura iterativa di Layer B guidata dal rilevamento: ogni round genera varianti
--candidates(predefinito 1,WATERMARKS_REWRITE_CANDIDATES) e--max-loops(predefinito 1,WATERMARKS_REWRITE_LOOPS) limita i round di valutazione, fermandosi non appena un tentativo supera il rilevamento. Priorità dell'evaluator: MarkLLM (--markllm-scheme) > divergenza lessicale bigram-Jaccard (fallback).rewrite_text.py --json-statsora riportaevaluator/max_loops/attempts_made/passedecandidate_scoresper tentativo (#153) - Verifica same-key Keyed-Gumbel (Aaronson EXP): il nuovo
detect_gumbel.pysolo-stdlib implementa il test di replay model-free (u = PRF(Hash(key, window), token); p-value esatto della coda Gamma; mascheramento delle finestre ripetute) senza GPU, modello o logits.rewrite_text.py --gumbel-key(envWATERMARKS_GUMBEL_KEY, preferito) lo rende l'evaluator del loop iterativo (priorità: gumbel > markllm > divergenza lessicale) ed è esposto comegumbelin/capabilitiese/detect. Solo same-key — non un oracolo del fornitore; la chiave non viene mai registrata nei log (#190) - Benchmark: benchmark e rilevamento di testo MarkLLM multi-scheme (#188) e un benchmark riproducibile di rimozione SynthID-text (#145); varianti predefinite
paraphrase:3; report e CSV riportano i tentativi per documento (colonnemean_attempts/att,attempts/evaluator/passed);--rewrite-loopsrispecchia--max-loops - Rilevamento: rilevamento watermark testuale del fornitore (Gemini SynthID, Claude seam, MarkLLM) più un sidecar scorer SynthID per immagini (#109); nuovo rilevatore di testo AI statistico e stilometrico zero-LLM per CI e audit (#68, #69)
Distribuzione: installazioni di plugin, hook e skill
- Il repository è ora un plugin Claude Code e un marketplace a plugin singolo (
.claude-plugin/plugin.json+marketplace.json), quindi entrambe le skill si installano con/plugin marketplace add guillaumemeyer/watermarks-removerpoi/plugin install watermarks-remover@watermarks-remover, e si aggiornano in place.make plugin-validateesegueclaude plugin validate . --strict;tests/test_plugin_manifest.pycontrolla i manifest senza la CLI install_skill.pyha guadagnato un--target(claude-code,claude-project,cowork,cursor) e un selettore--skillche copre entrambe le skill distribuite, più--list,--linkeCLAUDE_CONFIG_DIR. Il targetcoworkcostruisce un bundle di upload riproducibile (dist/<skill>.zip, singola directory skill di primo livello); ogni target viene validato rispetto alle regole di packaging Agent Skills e al limite di upload di 30 MB. Nuovi targetmake:install-claude-code-skill,install-claude-code-text-skill,install-claude-project-skill,package-cowork-skill,package-cowork-text-skill- Auto-pulizia deterministica tramite un hook
PostToolUse(hooks/hooks.json+service/scripts/hook_written_file.py): dopo che l'agente scrive un file, l'harness esegue l'hook indipendentemente dalla cooperazione del modello.check(predefinito) segnala i marchi al modello;cleanli rimuove in place e comunica al modello che il file è stato spostato, scambiando solo in caso di differenza reale così i file clean mantengono il loro mtime. La modalità proviene dall'impostazionehook_modedel plugin o daWATERMARKS_HOOK_MODE; il rilevamento riutilizzaaudit_lib.scan_file/is_actionable, quindi l'hook, il gate pre-commit e l'export SARIF della CI concordano. Un hook non può comunque riscrivere il messaggio di chat dell'assistente — non esiste un tale punto di hook — quindi quel percorso rimane best-effort - Integrazione dell'hook pre-commit per il controllo/pulizia dei file in stage (#138); lightweight Cursor text skill (#35); la descrizione di
clean-user-facing-textnon nomina più Cursor come unico host
Servizio HTTP
- Endpoint batch:
POST /clean/batch,/inspect/batch(#137) ePOST /detect/batch(#151) - Preserva le estensioni del formato immagine in
/cleane usa scritture sicure inav_meta(#150); usa base64 portabile nell'esempio curl di/detect(e correggi la portabilità direalpathsu macOS nei bootstrap, #185)
Audit / ispezione e sicurezza
audit_dir.pyha guadagnato concorrenza multi-worker ed export SARIF 2.1.0 (#101, #102)- Instrada i formati binari del sito web verso i loro scanner reali (#177); rifiuta le bombe DTD/entity nel parser della sitemap (GHSA-pjg6-92pm-mmcf) (#146); un cleaner andato in crash blocca il commit invece di essere letto come clean (#179); un file di testo illeggibile è una scansione fallita, non una clean (#169)
Correzioni di affidabilità e correttezza
- Una seconda esecuzione
--in-placepreserva il.bakoriginale; mantiene le evidenze raccolte quando un membro zip successivo non riesce a essere letto (#175); i container ISOBMFF troncati eseguono comunque il fallback di scansione byte C2PA (#176); distingue un cleaner fallito da un file già clean (#159, #161); tratta un'esecuzione c2patool fallita come inconcludente piuttosto che "no C2PA" (#156); valida i tipi delle opzioni clean (#111); non seleziona mai automaticamente il dispositivo MPS per il rilevamento watermark testuale (#99); portabilità macOS — stdout puro--jsonper lo scorer SynthID e sondarealpathBSD (#70); corregge un percorsosubprocess_creationflagsdi Windows in_ghostscript_usablee impedisce ai processi figli di aprire una finestra console su Windows - Hardening comportamentale: preserva i commenti JPEG benigni in keep-mode; corregge il flag inghiottito di
bench-synthid-text; semplifica il passthrough dei flag per la sonda Ghostscript e il noqa non necessario di clean_text (lint)
CI / tooling / documentazione
- Linting e formattazione Ruff con applicazione in CI (#103); aggiunge macOS alla matrice di test (#152); aggiunge una configurazione CodeRabbit per le revisioni automatiche delle PR (#222); CODEOWNERS per CODE_OF_CONDUCT/LICENSE e i proprietari della revisione principale; attribuisce il copyright a Guillaume Meyer e ai contributori (#228)
- Documentazione: guida alla riscrittura che preserva la voce e protezione delle scelte di voce/accessibilità; aggiunte all'Ecosystem (ClaudeWatermarks, unmark-web) e una nota che scoraggia nomi simili; riferimento arXiv 2402.14904; guida all'avvio automatico su Windows tramite Task Scheduler; base64 portabile negli esempi curl; fissa il motore di testo vendored della Cursor-skill alla copia del servizio (#96)
Unreleased
- Hook clean pre-commit (
watermarks-remover-clean/clean_staged.py): usa digest del contenuto (SHA-256) e rilevamento attivo delle azioni così i file clean su disco vengono riconosciuti senza richiedere infiniti re-staging (#173) - Preservazione del container OOXML: mantiene
<AppVersion>intatto indocProps/app.xmldurante la pulizia dei metadati DOCX, XLSX e PPTX per soddisfare i vincoli dello schema ECMA-376 ed evitare errori di "contenuto illeggibile" di Microsoft Word/Office (#283)
v0.5.0 — distribuzione servizio e Docker, API HTTP e harness di verifica
Distribuzione servizio / Docker
- Separazione skill/servizio: la skill (
skills/remove-ai-marks/) è ora un client remoto senza codice su HTTP; tutta l'implementazione è stata spostata inservice/scripts/e gira dietroserver.py, un entrypoint HTTP stdlib (/health,/inspect,/clean,/capabilities) - Servizio HTTP:
service/scripts/server.pyespone la pipeline di pulizia su JSON/base64; l'hardening rispecchia le CLI (limiti di dimensione, guardia binaria, scritture atomiche, loopback predefinito, autenticazione bearer opzionaleWATERMARKS_SERVER_API_KEY) - OpenAPI:
GET /openapi.jsonserve una specifica OpenAPI 3.0.3 generata dinamicamente (costruita dalla tabella delle route + configurazione live, quindi non si discosta mai dagli endpoint reali); la CI la valida conopenapi-spec-validator - Immagine Docker core (
service/Dockerfile): servizio di pulizia completo con exiftool / qpdf / c2patool preinstallati; qualsiasi CLI rimane eseguibile sovrascrivendo il comando - Docker / compose:
compose.yamlavvia l'intera infrastruttura (coresempre;markllm/markdiffusiondietroprofile: harness;ctrlregen/synthiddietroprofile: heavycome build solo locali); i servizi hanno il prefissowr-; i servizi harness/heavy hanno come predefinitocommand: ["--help"]cosìdocker compose up --profile harness --profile heavytermina in modo pulito (le CLI one-shot vengono eseguite condocker compose run); nuovimake compose-check/compose-check.shvalidano lo stack in esecuzione (solo codice di uscita) - Pubblicazione GHCR:
.github/workflows/release-images.ymlpubblica le immaginicore,markllm,markdiffusionsui tagv*;ctrlregen/synthidnon vengono mai pubblicati (licenza upstream) - Configurazione env:
.env.example+ guida alla configurazione del servizio;docker composecarica automaticamente.env;.envè in gitignore (deny-by-default) - Igiene del repo:
.gitignoreeservice/.dockerignoresono ora deny-by-default — solo i percorsi esplicitamente consentiti possono essere committati o inviati in un contesto di build (i contesti delle immagini spediscono soloservice/scripts/, che è tutto ciò che i Dockerfile copiano) - Test:
tests/test_http_server.py(13 casi) per il servizio HTTP; tutte le suite reindirizzate aservice/scripts/
Harness MarkDiffusion per watermark su immagini (opzionale)
- Nuovo harness opzionale (esterno
THU-BPM/MarkDiffusion, Apache-2.0):markdiffusion_harness.pycon sottocomandiwatermark/detect/purifyper nove schemi immagine (Tree-Ring, Ring-ID, ROBIN, WIND, SFW, Gaussian-Shading, GaussMarker, PRC, SEAL) clean_image.py --remove-pixel diffusionesegue l'attacco di rigenerazioneDiffusionPurificationdi MarkDiffusion come motore alternativo di rimozione pixel (intensità conservativa 0.3 predefinita)- Bootstrap
setup_markdiffusion.sh(pin PyPI1.0.2; clone editabile--checkouta commit fissato) +requirements-markdiffusion.txt+Dockerfile.markdiffusione Makefilebootstrap-markdiffusion/smoke-markdiffusion/docker-markdiffusion-build/docker-markdiffusion-help - Test basati su mock (
tests/test_markdiffusion_harness.py) — nessun torch in CI; documento di riferimentoreferences/markdiffusion.md - Documentazione: avvertenza sulla verifica solo same-scheme (non un oracolo del rilevatore del fornitore) e avvertenza sulla deriva della rigenerazione cieca in README, SKILL.md,
removal-matrix.md,markdiffusion.md
Harness MarkLLM per watermark testuale (opzionale)
- Nuovo harness opzionale (checkout esterno
THU-BPM/MarkLLM, Apache-2.0):detect_text_watermark.pycon sottocomandidetect/watermarkper gli schemi KGW e SynthID rewrite_text.py --markllm-schemeesegue il rilevamento prima/dopo attorno a una riscrittura di Layer B e il rilevamento per-candidato quando--candidates N>1(env-gated; riportacleared)- Bootstrap
setup_markllm.sh+requirements-markllm.txt(dipendenze fissate) +Dockerfile.markllme Makefilebootstrap-markllm/smoke-markllm/docker-markllm-build/docker-markllm-help - Hardening: caricamento del modello solo da cache
--offline(nessun egress HF, nessun codice remoto), limite di 1 MiB per la configurazione,WATERMARKS_MARKLLM_RLIMIT_ASopzionale sul sottoprocesso di riscrittura, torch fissato nel Dockerfile e verifica del clone-SHA inDockerfile.markllm - Test basati su mock (
tests/test_markllm_detect.py, 21 casi) — nessun torch in CI; avvertenza sull'harness di verifica (solo same-config, non un oracolo del rilevatore del fornitore) documentata in README, SKILL.md,removal-matrix.md,vendor-notes.md
Correzioni e rifiniture- Layer B: rewrite_text.py ora invia reasoning_effort: "none" per impostazione predefinita per i backend openai-compatible (--reasoning-effort / WATERMARKS_REWRITE_REASONING_EFFORT; off lo omette). I modelli di ragionamento come deepseek-v4-flash altrimenti consumano ~100s di chain-of-thought per una riscrittura di una riga (9.894 vs 12 token di completamento)
- Correzione build immagine markllm:
requirements-markllm.txtfissavatokenizers==0.23.1, che entra in conflitto contransformers==5.15.0(limitatokenizers<=0.23.0; non esiste una release 0.23.0) — ora fissato atokenizers==0.22.2; torch spostato sull'indice wheel CPU (torch==2.13.0.*) così l'immagine è solo-CPU comeDockerfile.markdiffusion - Correzione build immagine ctrlregen: i pin di ricerca dell'era 2023 (
safetensors==0.4.3,transformers==4.37.2→tokenizers<0.19) non forniscono wheel per Python 3.14, quindi l'immagine base è orapython:3.11-slim(fissata per digest, multi-arch) - Correzione immagini harness a runtime:
Dockerfile.markllmeDockerfile.markdiffusionnon copiavano maicommon.pyin/app(bug preesistente) — aggiunto - WebP: ispezione e pulizia dei metadati solo-stdlib per i chunk RIFF
C2PA, XMP, EXIF e profilo ICC (#37) - BMP / GIF / TIFF: rilevamento, ispezione e pulizia dei metadati solo-stdlib — le estensioni commento/XMP GIF vengono eliminate mentre il looping
NETSCAPE2.0è preservato; i metadati IFD TIFF (XMP/EXIF/GPS/IPTC/MakerNote) vengono eliminati con i payload azzerati e gli offset delle strip mantenuti, sia per il TIFF classico che per BigTIFF; i metadati in coda BMP vengono troncati con il campo dimensione file riscritto - EPUB: pulizia del container solo-stdlib — metadati OPF e meta/JSON-LD XHTML ripuliti, media raster/SVG incorporati rimossi, Layer A applicato al testo del corpo XHTML, parti di metadati portatrici di marker eliminate e parti cifrate OCF lasciate intatte
- Sanitizzazione nomi file: il servizio HTTP rifiuta nomi di output non sicuri forniti dal client
- Correzione del cleaner frontmatter markdown che andava in crash e perdeva chiavi AI annidate (#25)
- Gli strumenti di testo rifiutano input binari;
--force-textlo sovrascrive (#24) --jsonnon sopprime più il codice di uscita del segnale residuo (#30)inspect_filestampa il nome del file nel suo output (#50)- Preserva i meta tag generator CMS con maiuscole/minuscole miste (#42)
- Preserva gli invisibili portanti degli script, rimuovi PUA in Layer A (#38, #52)
- Preserva i joiner degli script, le emoji bandiera e i segni Cf arabi in Layer A (#28)
- Rafforza l'audit del sito web contro SSRF e bombe gzip (#49)
- SECURITY.md fa riferimento solo al canale degli advisory privati (#51)
- Windows: porte PowerShell dei bootstrap di setup (#40)
- Documentazione: aggiunti shield stars/forks e rimosso il grafico star-history; aggiunto MarkLLM ai riferimenti del README; template di pull request; piano per il deployment Docker CLI + API
v0.4.0 — rimozione a livello pixel, confidenza dei finding, correzioni Windows e falsi positivi
Rimozione pixel opzionale CtrlRegen (backend esterno)
- Rimozione opzionale della filigrana nel dominio dei pixel tramite un checkout esterno
mertizci/noai-watermark: adattatoreclean_ctrlregen.py+ bootstrapsetup_ctrlregen.sh(commit fissato, sparse checkout, venv, verifica SHA), piùDockerfile.ctrlregenemake bootstrap-ctrlregen/docker-ctrlregen-build/smoke-ctrlregen clean_image.py --remove-pixel ctrlregenesegue strip dei metadati → rimozione CtrlRegen → punteggio reverse-SynthID opzionale prima/dopo;inspect_image.pysuggerisce il flag in caso di punteggio SynthID elevato- Intensità predefinita conservativa
0.25(preset 0.15/0.25/0.35/0.5/0.7); la pipeline nativa 512×512 viene auto-tiled dal backend per immagini più grandi; il sottoprocesso torch riceve limiti di risorse più alti sovrascrivibili via env - Il backend non è mai incluso:
noai-watermarknon fornisce alcun file LICENSE (trattato come tutti i diritti riservati), e i suoi percorsi di auto-installazione/riavvio vengono aggirati usando direttamenteCtrlRegenEngine
Confidenza dei finding e audit aggregati
- I finding sono ora classificati
confirmed/probable/informational/likely_false_positive, esposti nei report JSON e umani di testo/immagine/container - Nuovi
audit_dir.py(albero ricorsivo) eaudit_website.py(scoperta sitemap + crawl) che aggregano i report; documentati in SKILL.md
Correzioni falsi positivi
- DOCX: scansiona solo
docProps/customXml, non il corpo visibile (#14) - Text Layer A: preserva emoji
VS16/ZWJdopo una base emoji; nuovo flag paranoico--strip-emoji-glue(#22) - HTML: tratta i tag generator CMS come informativi, non come metadati AI (#13)
- PDF: esclude i payload degli stream dalla scansione byte dei marker AI (#13)
- I report di ispezione annotano i percorsi non supportati/best-effort
Supporto Windows
- Condiziona
preexec_fneos.fchmodsolo-POSIX così che le scritture e gli strumenti opzionali funzionino su Windows (#15, #23) - Riconfigura stdio su UTF-8 così che gli stream Windows reindirizzati non sollevino più eccezioni su Unicode invisibile; leg CI Windows + esecuzione smoke CLI (#23)
Documentazione e supply chain
- Sezione CtrlRegen nel README + riferimenti di ricerca (CtrlRegen, UnMarker, avvertenza forensic-stealth), disclaimer sull'uso responsabile; aggiornamenti a SKILL/matrix/vendor-notes/ethics
- Configurazione Dependabot + CODEOWNERS per i percorsi di sicurezza; aggiornati scipy/numpy/opencv-python/scikit-learn/pywavelets e l'immagine base a Python 3.14-slim
- Test CtrlRegen basati su mock (nessun torch in CI)
v0.3.2 — rafforzamento della sicurezza (scritture sicure, client HTTP, supply chain CI)
- Scritture di output sicure e atomiche: ogni cleaner ora scrive tramite file temporaneo + rename atomico (
safe_write_bytes/safe_write_text), rifiuta destinazioni symlinkate e crea backup.bakattraverso lo stesso percorso sicuro — i symlink pre-posizionati (ad es. in/tmpo nelle directory di download) non possono più reindirizzare una scrittura pulita su un file arbitrario - Rafforzamento del client HTTP di
rewrite_text.py: i redirect vengono rifiutati categoricamente, così una chiave API nell'headerAuthorizationnon può mai essere re-inviata a un host non validato; gli endpoint non-loopback sono negati per impostazione predefinita (opt-in con--allow-remoteoWATERMARKS_REWRITE_ALLOW_REMOTE=1); sono accettati solo schemi http(s);--api-keyè stato rimosso — le chiavi sono solo via env tramiteWATERMARKS_REWRITE_API_KEY - Limiti di risorse: input massimo predefinito da 1 GiB → 256 MiB, nuovo limite stdin di 64 MiB, budget zip DOCX/ODT da 512 MiB → 128 MiB, e
RLIMIT_AS/RLIMIT_FSIZEapplicati ai sottoprocessi exiftool/c2patool/SynthID (tutti i limiti sovrascrivibili via env) - Supply chain: azioni CI fissate per SHA con
permissions: contents: read, dipendenze dev fissate (requirements-dev.txt), uno steppip-audite un nuovo workflow CodeQL; l'immagine Docker ora gira come utente non privilegiato con pip fissato - Dipendenze dello scorer: Pillow aggiornato da 10.4.0 → 12.3.0 (24 CVE note); utilizzo dell'API verificato rispetto al commit upstream fissato
- Test: 18 nuovi test di regressione di sicurezza (60 totali, tutti superati)
v0.3.1 — riscrittura più forte della filigrana statistica Layer B
- La parafrasi predefinita di
rewrite_text.pyora esegue un attacco esplicito di scelta delle parole + sintassi (ordine delle clausole, connettivi, parole di transizione, confini di frase, parole funzionali) invece di una riscrittura generica - Nuovo
--tactic humanize: passata zero-shot "scrivi come un umano" mirata a frasi formulari in stile AI - Nuovo
--tactic code: riscrive commenti, docstring e stringhe letterali, e rinomina identificatori locali preservando comportamento e nomi dell'API pubblica - La passata strutturale ora produce "prosa umana naturale e varia" invece del tipico "stile professionale chiaro" dell'AI
- Nuovo
--temperature(predefinito0.9) per entrambi i backend Ollama e OpenAI-compatible - Nuovo
--candidates N: genera N riscritture e seleziona quella più divergente lessicalmente (distanza Jaccard sui bigrammi) con una protezione contro la deriva di lunghezza - Igiene del modello più forte: preferisci modelli locali open-weight ed evita qualsiasi fornitore noto per filigrane, non solo l'origine sospetta
- Il reporting del rischio residuo ora distingue testo breve/altamente prevedibile (rischio inferiore) da prosa lunga ad alta entropia (rischio superiore)
- Documentazione aggiornata in
SKILL.md,removal-matrix.mdevendor-notes.md; i test coprono i nuovi prompt, lo scoring di divergenza e la selezione dei candidati
v0.3.0 — scoring pixel SynthID opzionale
- Scorer SynthID opzionale nel dominio dei pixel tramite un checkout esterno
aloshdenny/reverse-SynthID(score_synthid.py); esposto ininspect_image.py/clean_image.pyconREVERSE_SYNTHID_DIRo--synthid-dir - Bootstrap
setup_synthid.sh(dipendenze solo dello scorer;--fullinstalla i requisiti upstream);Dockerfile.synthidpiùmake docker-synthid-build/docker-synthid-help - Target Makefile
smoke-synthidebootstrap-synthid - Test per l'adattatore dello scorer, il percorso CLI non disponibile, il parsing JSON e gli errori a runtime
- Documentazione: solo rilevamento/scoring (nessuna rimozione a livello pixel); il codice upstream non è incluso e rimane sotto la sua Research License non commerciale
v0.2.0 — correzione falso positivo c2patool
image_meta.py:has_manifestnon segnala piùError: No claim found/No JUMBF data foundcome manifest (bug di precedenza degli operatori: i marker negativi ora pongono il veto su ogni ramo positivo)- Nuovo
tests/test_c2patool_report.py(4 casi: no claim, no JUMBF, manifest genuino, strumento assente) - Documentazione: corretti i link a
c2patool(repo spostato incontentauth/c2pa-rs); aggiunto un disclaimer sul costo in qualità della rimozione della filigrana testuale
v0.1.0 — rifinitura del packaging + onestà sulla provenienza
Makefile(test/smoke/install-skill) epytest.ini- Campioni fixture per Markdown, HTML, SVG; test di pulizia degradata PDF
- Documentazione: modello industriale a due livelli (C2PA hard-bound vs soft binding / SynthID-media)
- Tabella del rischio residuo nel README + link a strumenti di verifica esterni
- Riferimento: guida C2PA/SynthID dell'Institute of AI PM
- Soft-binding e filigrane pixel/audio/video esplicitamente fuori ambito in skill/matrix/ethics
v0.0.1 — release iniziale multi-vendor
- Skill agente
remove-ai-marks(sostituisceremove-claude-markssolo per Claude) - Layer A: Unicode invisibile / bidi / caratteri tag / omoglifi spazio (
inspect_text/clean_text) - Layer B: guida alla riscrittura +
rewrite_text.pyopzionale (print-prompt, Ollama, OpenAI-compatible) - File: strip metadati C2PA/AI per PNG, JPEG, SVG, PDF, DOCX, ODT, HTML, Markdown
inspect_file.py/clean_file.pyunificati- Documentazione multi-vendor (Claude, Gemini/classe SynthID, OpenAI, open-LLM)
- Script stdlib-first;
c2patool/exiftoolopzionali
Licenza
MIT — vedi LICENSE.
Bibliografia
- How Claude marks AI-generated content (Anthropic)
- Dathathri et al., Scalable watermarking for identifying large language model outputs (SynthID-Text, Nature 2024)
- Google AI for Developers, SynthID safeguards (documentazione API Gemini)
- C2PA / c2patool
- Kirchenbauer et al., A Watermark for Large Language Models
- Evseev, D. (Arbitration City), Accurate, Costless, and Invisible AI Text Watermarking for Self-Hosted AI Inference (rapporto tecnico, agosto 2026) — watermarking keyed-Gumbel incluso nel motore open-source arbi-serve, con rilevamento exact-test e supporto speculative-decoding — PDF
- THU-BPM/MarkLLM (toolkit unificato per valutare algoritmi di watermarking LLM)
- Pan et al., MarkDiffusion: An Open-Source Toolkit for Generative Watermarking of Latent Diffusion Models (JMLR) — il toolkit di embedding che l'harness opzionale per filigrane su immagini di questo repo avvolge — code, docs
- Zhang et al., Watermarks in the Sand: Impossibility of Strong Watermarking for Generative Models (ICML 2024)
- Sander et al., Watermarking Makes Language Models Radioactive — le filigrane sopravvivono al fine-tuning e marcano i modelli downstream addestrati su dati con filigrana
- Pan et al., Can LLM Watermarks Robustly Prevent Unauthorized Knowledge Distillation? — provenienza basata su filigrana e protezione contro la knowledge distillation
- google-deepmind/synthid-text (riferimento di ricerca; non usato qui per il rilevamento)
- aloshdenny/reverse-SynthID (riferimento di ricerca)
- ETH Zurich SRI, Probing SynthID (blog di ricerca sulla rilevabilità delle filigrane SynthID)
- Liu et al., Image Watermarks are Removable Using Controllable Regeneration from Clean Noise (ICLR 2025) — il metodo di rigenerazione a livello pixel che il backend opzionale CtrlRegen implementa — code
- Kassis & Hengartner, UnMarker: A Universal Attack on Defensive Image Watermarking (arXiv:2405.08363; IEEE S&P 2025) — un attacco universale alle filigrane confrontato su una metrica diversa rispetto a CtrlRegen
- Goonatilake & Ateniese, Removing the Watermark Is Not Enough: Forensic Stealth in Generative-AI Watermark Removal (arXiv:2605.09203) — motiva il default di intensità conservativa: la rimozione può comunque lasciare tracce forensi
- mertizci/noai-watermark (toolkit CLI/Python per la rimozione di SynthID/StableSignature/TreeRing e lo strip dei metadati AI)
- 0xROOTPLS/DeSynth (rimozione SynthID per immagini OpenAI/Google)
- Institute of AI PM, AI Content Provenance and Watermarking: The PM's Guide to C2PA and SynthID (modello industriale a due livelli: C2PA + filigrana impercettibile / soft binding; contesto SB 942 / EU AI Act Art. 50)