Torna agli aggiornamenti
New releaseAug 30, 2026

watermarks-remover v0.5.0

Un'app che mette al primo posto la privacy e rimuove le filigrane AI dai contenuti di tua proprietà.

Condividi
_ _ _ ____ ___ ____ ____ _  _ ____ ____ _  _ ____    ____ ____ _  _ ____ _  _ ____ ____
| | | |__|  |  |___ |__/ |\/| |__| |__/ |_/  [__  __ |__/ |___ |\/| |  | |  | |___ |__/
|_|_| |  |  |  |___ |  \ |  | |  | |  \ | \_ ___]    |  \ |___ |  | |__|  \/  |___ |  \

watermarks-remover

CI Release Stars Forks

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.

LivelloObiettivoCome
AUnicode invisibile, spazi esotici, bidi, tag charsScript Python deterministici
BWatermark testuali statistici (token-sampling)Riscrittura dell'agent + hook opzionale rewrite_text.py
FileC2PA / EXIF / XMP / proprietà documentoPNG, 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):

StrumentoRuolo
c2patoolIspeziona i manifest C2PA
exiftoolRimozione dei metadati residui (spec. PDF)
qpdfRicostruzione 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:

MetodoPercorsoCorpoRestituisce
GET/health{"ok": true, "version": ...}
GET/capabilitiesstrumenti / backend opzionali utilizzabili (ogni strumento è verificato nella versione, non solo trovato nel PATH)
GET/openapi.jsonspecifica 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:

  • --offline sull'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 (il trust_remote_code di 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:

  1. 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.
  2. Motore opzionale di rimozione a livello di pixel: il suo attacco di rigenerazione DiffusionPurification è esposto come clean_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.
  3. 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:

  1. Lossless. pdfwrite con 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.
  2. 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):

CanaleCosa rimuoviamoCosa può rimanereVerifica esterna (esempi)
C2PA hard-bound / EXIF / XMPMarchi soft-bound / nei pixelc2patool, Content Credentials verify
Media classe SynthIDRimozione opzionale dei pixel (CtrlRegen esterno); altrimenti punteggio localeWatermark audio/video; watermark residuo nei pixel dopo la rimozioneStrumenti del provider (es. Google SynthID / rilevatore Vertex dove offerto); scorer locale opzionale reverse-SynthID
Testo statisticoRiscrittura best-effortMarchi forti dopo modifiche leggereNessun 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.

OpzioneRimuoveNote
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 stripProvenienza del fileVedi 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-weightEvitare di ri-marchiare con il modello di origineAlternativa 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:

`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

  • /clean esegue la riscrittura di Layer B per il testo dopo Layer A. Il valore predefinito proviene da config/clean_strategy.json; un options.strategy per-richiesta lo sovrascrive, e /clean rifiuta 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 con roberta-large — una modifica locale non autoregressiva, quindi l'output mescola il flusso di token originale con le predizioni del masked-LM (#311).
  • La tattica humanize ora applica deterministicamente il passaggio humanizer-skill (virgolette dritte, nessun trattino en/em, collasso dei riempitivi, utilizeuse) e nomina le regole dello scrittore umano nel prompt (#311). rewrite_text.py ha 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 uuid di 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/iTXt PNG decompresso (#308); rimuove le dichiarazioni DOCTYPE/ENTITY XML negli SVG (#288); mantiene i membri binari DOCX byte-safe (#314); preserva AppVersion in OOXML (#289).

Servizio HTTP e CLI

  • Opzione /clean per mantenere spazi esotici, in linea con la CLI (#274); /inspect espone 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 a inspect_* per evitare una rilettura ridondante.
  • clean_file.py ha 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.py scansiona file sorgente, documentazione e i18n che il router ha saltato (#284); scansiona .ts/.tsx/.jsx/.gd e allinea la confidenza degli spazi tra i formati (#273); supporto audit_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.0 e 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 docProps dei DOCX; elimina le relazioni pendenti dopo la rimozione di customXml; 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_Ignorable senza un uso legittimo di interscambio (U+2065, U+FFF0U+FFF8, U+E0000, U+E0080U+E00FF, U+E01F0U+E0FFF — segnalati come reserved_ignorable), i 66 noncaratteri (U+FDD0U+FDEF più U+FFFE/U+FFFF per piano — segnalati come noncharacter) e tre portatori Default_Ignorable a rendering vuoto che il catch-all Cf non 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+13430U+1343F), i controlli della stenografia Duployan (U+1BCA0U+1BCA3) e i controlli musicali beam/tie/slur/phrase (U+1D173U+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-glue li 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-stats ora riporta evaluator / max_loops / attempts_made / passed e candidate_scores per tentativo (#153)
  • Verifica same-key Keyed-Gumbel (Aaronson EXP): il nuovo detect_gumbel.py solo-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 (env WATERMARKS_GUMBEL_KEY, preferito) lo rende l'evaluator del loop iterativo (priorità: gumbel > markllm > divergenza lessicale) ed è esposto come gumbel in /capabilities e /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 (colonne mean_attempts / att, attempts / evaluator / passed); --rewrite-loops rispecchia --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-remover poi /plugin install watermarks-remover@watermarks-remover, e si aggiornano in place. make plugin-validate esegue claude plugin validate . --strict; tests/test_plugin_manifest.py controlla i manifest senza la CLI
  • install_skill.py ha guadagnato un --target (claude-code, claude-project, cowork, cursor) e un selettore --skill che copre entrambe le skill distribuite, più --list, --link e CLAUDE_CONFIG_DIR. Il target cowork costruisce 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 target make: 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; clean li 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'impostazione hook_mode del plugin o da WATERMARKS_HOOK_MODE; il rilevamento riutilizza audit_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-text non nomina più Cursor come unico host

Servizio HTTP

  • Endpoint batch: POST /clean/batch, /inspect/batch (#137) e POST /detect/batch (#151)
  • Preserva le estensioni del formato immagine in /clean e usa scritture sicure in av_meta (#150); usa base64 portabile nell'esempio curl di /detect (e correggi la portabilità di realpath su macOS nei bootstrap, #185)

Audit / ispezione e sicurezza

  • audit_dir.py ha 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-place preserva il .bak originale; 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 --json per lo scorer SynthID e sonda realpath BSD (#70); corregge un percorso subprocess_creationflags di Windows in _ghostscript_usable e 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 in docProps/app.xml durante 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 in service/scripts/ e gira dietro server.py, un entrypoint HTTP stdlib (/health, /inspect, /clean, /capabilities)
  • Servizio HTTP: service/scripts/server.py espone la pipeline di pulizia su JSON/base64; l'hardening rispecchia le CLI (limiti di dimensione, guardia binaria, scritture atomiche, loopback predefinito, autenticazione bearer opzionale WATERMARKS_SERVER_API_KEY)
  • OpenAPI: GET /openapi.json serve 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 con openapi-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.yaml avvia l'intera infrastruttura (core sempre; markllm / markdiffusion dietro profile: harness; ctrlregen / synthid dietro profile: heavy come build solo locali); i servizi hanno il prefisso wr-; i servizi harness/heavy hanno come predefinito command: ["--help"] così docker compose up --profile harness --profile heavy termina in modo pulito (le CLI one-shot vengono eseguite con docker compose run); nuovi make compose-check / compose-check.sh validano lo stack in esecuzione (solo codice di uscita)
  • Pubblicazione GHCR: .github/workflows/release-images.yml pubblica le immagini core, markllm, markdiffusion sui tag v*; ctrlregen / synthid non vengono mai pubblicati (licenza upstream)
  • Configurazione env: .env.example + guida alla configurazione del servizio; docker compose carica automaticamente .env; .env è in gitignore (deny-by-default)
  • Igiene del repo: .gitignore e service/.dockerignore sono ora deny-by-default — solo i percorsi esplicitamente consentiti possono essere committati o inviati in un contesto di build (i contesti delle immagini spediscono solo service/scripts/, che è tutto ciò che i Dockerfile copiano)
  • Test: tests/test_http_server.py (13 casi) per il servizio HTTP; tutte le suite reindirizzate a service/scripts/

Harness MarkDiffusion per watermark su immagini (opzionale)

  • Nuovo harness opzionale (esterno THU-BPM/MarkDiffusion, Apache-2.0): markdiffusion_harness.py con sottocomandi watermark / detect / purify per nove schemi immagine (Tree-Ring, Ring-ID, ROBIN, WIND, SFW, Gaussian-Shading, GaussMarker, PRC, SEAL)
  • clean_image.py --remove-pixel diffusion esegue l'attacco di rigenerazione DiffusionPurification di MarkDiffusion come motore alternativo di rimozione pixel (intensità conservativa 0.3 predefinita)
  • Bootstrap setup_markdiffusion.sh (pin PyPI 1.0.2; clone editabile --checkout a commit fissato) + requirements-markdiffusion.txt + Dockerfile.markdiffusion e Makefile bootstrap-markdiffusion / smoke-markdiffusion / docker-markdiffusion-build / docker-markdiffusion-help
  • Test basati su mock (tests/test_markdiffusion_harness.py) — nessun torch in CI; documento di riferimento references/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.py con sottocomandi detect / watermark per gli schemi KGW e SynthID
  • rewrite_text.py --markllm-scheme esegue il rilevamento prima/dopo attorno a una riscrittura di Layer B e il rilevamento per-candidato quando --candidates N>1 (env-gated; riporta cleared)
  • Bootstrap setup_markllm.sh + requirements-markllm.txt (dipendenze fissate) + Dockerfile.markllm e Makefile bootstrap-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_AS opzionale sul sottoprocesso di riscrittura, torch fissato nel Dockerfile e verifica del clone-SHA in Dockerfile.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.txt fissava tokenizers==0.23.1, che entra in conflitto con transformers==5.15.0 (limita tokenizers<=0.23.0; non esiste una release 0.23.0) — ora fissato a tokenizers==0.22.2; torch spostato sull'indice wheel CPU (torch==2.13.0.*) così l'immagine è solo-CPU come Dockerfile.markdiffusion
  • Correzione build immagine ctrlregen: i pin di ricerca dell'era 2023 (safetensors==0.4.3, transformers==4.37.2tokenizers<0.19) non forniscono wheel per Python 3.14, quindi l'immagine base è ora python:3.11-slim (fissata per digest, multi-arch)
  • Correzione immagini harness a runtime: Dockerfile.markllm e Dockerfile.markdiffusion non copiavano mai common.py in /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-text lo sovrascrive (#24)
  • --json non sopprime più il codice di uscita del segnale residuo (#30)
  • inspect_file stampa 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: adattatore clean_ctrlregen.py + bootstrap setup_ctrlregen.sh (commit fissato, sparse checkout, venv, verifica SHA), più Dockerfile.ctrlregen e make bootstrap-ctrlregen / docker-ctrlregen-build / smoke-ctrlregen
  • clean_image.py --remove-pixel ctrlregen esegue strip dei metadati → rimozione CtrlRegen → punteggio reverse-SynthID opzionale prima/dopo; inspect_image.py suggerisce 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-watermark non fornisce alcun file LICENSE (trattato come tutti i diritti riservati), e i suoi percorsi di auto-installazione/riavvio vengono aggirati usando direttamente CtrlRegenEngine

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) e audit_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/ZWJ dopo 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_fn e os.fchmod solo-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 .bak attraverso lo stesso percorso sicuro — i symlink pre-posizionati (ad es. in /tmp o 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'header Authorization non può mai essere re-inviata a un host non validato; gli endpoint non-loopback sono negati per impostazione predefinita (opt-in con --allow-remote o WATERMARKS_REWRITE_ALLOW_REMOTE=1); sono accettati solo schemi http(s); --api-key è stato rimosso — le chiavi sono solo via env tramite WATERMARKS_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_FSIZE applicati 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 step pip-audit e 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.py ora 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 (predefinito 0.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.md e vendor-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 in inspect_image.py / clean_image.py con REVERSE_SYNTHID_DIR o --synthid-dir
  • Bootstrap setup_synthid.sh (dipendenze solo dello scorer; --full installa i requisiti upstream); Dockerfile.synthid più make docker-synthid-build / docker-synthid-help
  • Target Makefile smoke-synthid e bootstrap-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_manifest non segnala più Error: No claim found / No JUMBF data found come 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 in contentauth/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) e pytest.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 (sostituisce remove-claude-marks solo per Claude)
  • Layer A: Unicode invisibile / bidi / caratteri tag / omoglifi spazio (inspect_text / clean_text)
  • Layer B: guida alla riscrittura + rewrite_text.py opzionale (print-prompt, Ollama, OpenAI-compatible)
  • File: strip metadati C2PA/AI per PNG, JPEG, SVG, PDF, DOCX, ODT, HTML, Markdown
  • inspect_file.py / clean_file.py unificati
  • Documentazione multi-vendor (Claude, Gemini/classe SynthID, OpenAI, open-LLM)
  • Script stdlib-first; c2patool / exiftool opzionali

Licenza

MIT — vedi LICENSE.

Bibliografia

Categorie