
watermarks-remover v0.5.0
Une application axée sur la confidentialité qui supprime les filigranes IA du contenu que vous possédez.
_ _ _ ____ ___ ____ ____ _ _ ____ ____ _ _ ____ ____ ____ _ _ ____ _ _ ____ ____
| | | |__| | |___ |__/ |\/| |__| |__/ |_/ [__ __ |__/ |___ |\/| | | | | |___ |__/
|_|_| | | | |___ | \ | | | | | \ | \_ ___] | \ |___ | | |__| \/ |___ | \
watermarks-remover
Compétence d'agent + service Python stdlib pour supprimer les marques de provenance IA multi-fournisseurs du texte et des fichiers — pour la confidentialité et l'hygiène sur du contenu que vous possédez. La compétence est un client léger : elle pilote la mécanique via HTTP, donc l'hôte de l'agent n'a pas besoin de Python.
| Couche | Cible | Comment |
|---|---|---|
| A | Unicode invisible, espaces exotiques, bidi, caractères de balise | Scripts Python déterministes |
| B | Filigranes textuels statistiques (échantillonnage de tokens) | Réécriture par l'agent + hook optionnel rewrite_text.py |
| Fichiers | C2PA / EXIF / XMP / propriétés de document | PNG, JPEG, WebP, AVIF, HEIC, BMP, GIF, TIFF, SVG, PDF, DOCX, XLSX, PPTX, EPUB, ODT, HTML, Markdown, MP4/MOV/M4A/M4V, WAV, MP3, FLAC |
Fournisseurs / écosystèmes (au niveau des classes) : Claude, Gemini / SynthID-Text, surfaces de provenance OpenAI, marques open-LLM de style Kirchenbauer (green-list) et keyed-Gumbel / EXP (Aaronson).
Dernière version : v0.7.0
Chemin de la compétence : skills/remove-ai-marks/
Chemin du service : service/
(migration : anciennement remove-claude-marks ; l'alias slash /remove-claude-marks est toujours documenté)
Installation (compétence d'agent)
La compétence ne contient aucun code — elle appelle le service via HTTP. Installez la compétence (markdown uniquement) et démarrez le service, puis définissez WATERMARKS_SERVICE_URL si ce n'est pas http://127.0.0.1:8765.
Dans Claude Code, la voie la plus rapide est le marketplace de plugins intégré — pas de clone, et il se met à jour sur place. Partout ailleurs, un seul installateur couvre tous les hôtes pris en charge (Python 3.10+ stdlib, aucune dépendance) :```bash python3 install_skill.py --skill remove-ai-marks --target claude-code
| Hôte | Cible | Emplacement final |
| --- | --- | --- |
| Claude Code (personnel) | `--target claude-code` | `~/.claude/skills/<skill>` (respecte `CLAUDE_CONFIG_DIR`) |
| Claude Code (projet) | `--target claude-project --project-dir PATH` | `PATH/.claude/skills/<skill>` |
| Cowork, claude.ai, sessions cloud, routines | `--target cowork` | `dist/<skill>.zip` à téléverser sous **Customize → Skills** |
| Cursor | `--target cursor` (par défaut) | `~/.cursor/skills/<skill>` |
Skills fournies : `remove-ai-marks` (complète, adossée à un service) et
`clean-user-facing-text` (texte uniquement, autonome). `--list` les affiche.
Les installations existantes sont préservées sauf si vous passez `--force` ; le
remplacement est d'abord mis en attente et l'installation précédente est
conservée comme sauvegarde nommée de manière unique.
`--link` crée un lien symbolique vers ce checkout au lieu de copier, de sorte que
les modifications sont prises en compte en direct. Sous Windows, utilisez
`py install_skill.py ...` ; le wrapper `install-skill.sh` est fourni pour les
shells macOS/Linux.
Avant d'écrire quoi que ce soit, l'installateur valide la skill selon les
règles d'empaquetage [Agent Skills](https://agentskills.io) que les téléversements
claude.ai et l'API Skills appliquent : frontmatter conforme à la spécification
(`name`, `description`, `license`, `compatibility`, `metadata`, `allowed-tools`),
un `name` en minuscules avec traits d'union d'au plus 64 caractères correspondant
au répertoire, une `description` non vide d'au plus 1024 caractères. Le bundle
Cowork doit en outre respecter la limite de téléversement de 30 Mo, ce que
l'empaqueteur impose.
### Nettoyage automatique via hook (déterministe)
Une skill est une instruction : le modèle décide de l'invoquer ou non, et le
modèle est ce qui produit les marques. Un **hook** est exécuté par le harnais
à chaque appel d'outil correspondant, sans nécessiter de coopération. Cela fait
du hook la moitié déterministe de ce flux de travail.
Le plugin enregistre un hook `PostToolUse` sur `Write|Edit|MultiEdit|NotebookEdit`
qui exécute [`service/scripts/hook_written_file.py`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/service/scripts/hook_written_file.py)
sur le fichier que l'agent vient d'écrire. Deux modes, correspondant à la
convention de pré-commit de vérification par défaut :
| Mode | Comportement |
| --- | --- |
| `check` (par défaut) | Signale les marques de provenance, laisse le fichier intact. Les résultats sont transmis au modèle (exit 2), afin qu'il puisse proposer de les nettoyer. |
| `clean` | Supprime les marques sur place, puis indique au modèle que le fichier sur disque a changé. |
Définissez le mode depuis les paramètres du plugin (**Hook mode** dans `/plugin manage`,
lu par le hook sous le nom `CLAUDE_PLUGIN_OPTION_HOOK_MODE`), ou avec
`WATERMARKS_HOOK_MODE=clean` dans l'environnement. La commande du hook
n'interpole délibérément **pas** `${user_config.hook_mode}` : Claude Code refuse
d'exécuter un hook qui référence une option que l'utilisateur n'a jamais ouverte
via `/plugin manage` pour la définir — un `default` déclaré ne suffit pas — donc
l'interpoler signifierait que le hook ne s'exécuterait jamais silencieusement sur
une installation neuve. La détection réutilise `scan_file` / `is_actionable` de
`audit_lib`, de sorte que le hook, la barrière de pré-commit et l'export SARIF de
la CI s'accordent sur ce qui compte comme actionnable ; le nettoyage délègue à
`clean_file.py`, de sorte qu'aucune logique de nettoyage n'est dupliquée. Le mode
`clean` écrit dans un fichier temporaire voisin et ne permute qu'en cas de
différence réelle, de sorte que les fichiers déjà propres conservent leur mtime
et ne redéclenchent pas les observateurs de fichiers.
Sans le plugin, câblez-le vous-même dans `~/.claude/settings.json` (ou un
`.claude/settings.json` de projet) :```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
}
]
}
]
}
}
Sur Windows, remplacez python3 par py.
Ce qu'un hook ne peut pas faire. Aucun hook ne peut réécrire le message de chat de l'assistant avant que vous ne le lisiez. Le hook Stop de Claude Code reçoit last_assistant_message en lecture seule, et il n'existe aucun filtre pré-envoi pour les réponses finales — la même limite que ce projet documente déjà pour les règles Cursor. Ainsi, la garantie déterministe couvre les fichiers que l'agent écrit, plus la barrière pre-commit pour tout ce qui se dirige vers git. Le texte qui n'existe que dans la transcription du chat dépend encore du workflow de la skill, qui est basé sur des instructions de modèle et donc au mieux de l'effort.
Plugin Claude Code (marketplace)
Le dépôt est aussi un plugin Claude Code et un marketplace à plugin unique (.claude-plugin/), donc les deux skills s'installent et se mettent à jour en deux commandes, sans clone ni script requis :```
/plugin marketplace add guillaumemeyer/watermarks-remover
/plugin install watermarks-remover@watermarks-remover
Les compétences se chargent alors avec un espace de noms : `/watermarks-remover:remove-ai-marks` et
`/watermarks-remover:clean-user-facing-text` (le simple `/remove-ai-marks` fonctionne aussi
lorsque rien d'autre ne revendique ce nom). `/plugin marketplace update
watermarks-remover` récupère les versions ultérieures. La même chose fonctionne depuis la CLI avec
`claude plugin marketplace add …` / `claude plugin install …`, et depuis un checkout local
en passant un chemin au lieu de `owner/repo`.
Mainteneurs : `make plugin-validate` exécute `claude plugin validate . --strict`
sur les deux manifestes ; `tests/test_plugin_manifest.py` couvre les mêmes fichiers
sans avoir besoin de la 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 récupère les compétences personnelles et de projet sans redémarrage ; /skills
liste ce qu'il a chargé. Invoquez avec /remove-ai-marks ou demandez de « supprimer les
filigranes IA / C2PA / marques Claude / texte de classe SynthID ». Une installation de projet est
aussi ce que lisent les sessions cloud,
puisqu'elles clonent le dépôt et chargent son .claude/skills/.
Cowork (et claude.ai, sessions cloud, routines)
Les sessions Cowork ne lisent pas ~/.claude/skills sur votre machine — elles chargent
les compétences activées pour votre compte claude.ai, synchronisées au démarrage de la session.
Donc installez-y en téléversant un bundle :```bash
python3 install_skill.py --skill remove-ai-marks --target cowork
writes dist/remove-ai-marks.zip (make package-cowork-skill)
Ensuite, dans l'application Claude Desktop, ouvrez **Customize → Skills → Add** et téléchargez le zip (les mêmes paramètres de skill sur claude.ai fonctionnent aussi). Le bundle est reproductible et contient un unique répertoire de premier niveau `remove-ai-marks/` avec `SKILL.md` à sa racine, ce qui correspond à la disposition attendue par l'upload.
L'accessibilité du service est plus importante ici que dans une installation locale : le skill est un client HTTP léger, donc la session doit pouvoir atteindre `WATERMARKS_SERVICE_URL`. Les sessions Cowork qui s'exécutent localement sur votre machine atteignent un `make serve` local ; les sessions cloud et les routines s'exécutent à distance et nécessitent une URL de service accessible depuis là-bas (et `WATERMARKS_SERVER_API_KEY` défini dessus). Si vous voulez un skill sans aucun service, téléchargez plutôt `clean-user-facing-text` — il est uniquement textuel et embarque ses propres scripts :```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
### Compétence facultative en texte uniquement
[`skills/clean-user-facing-text/`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/skills/clean-user-facing-text) est une
compétence autonome pour les manuscrits autorisés, la documentation et les
textes web. Elle exclut l'image, C2PA, le service et l'outillage de modèle
externe, et exécute ses propres scripts Layer A vendored au lieu d'appeler le
service.```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'invocation de compétence est sélectionnée par le modèle. Les projets qui adoptent explicitement ce flux de travail dans Cursor peuvent également copier la règle facultative :```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
Pour tous les projets, placez plutôt la même instruction dans les **User Rules** de Cursor.
Les règles améliorent la cohérence mais restent des instructions de modèle ; Cursor n'expose pas
de filtre déterministe avant envoi pour les réponses finales du chat.
### Démarrer le service
Le chemin le plus rapide est un serveur HTTP local (Python 3.10+ stdlib uniquement — aucune dépendance, pas de 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 (sans Docker)
Voir docs/windows-autostart.md pour démarrer automatiquement le service à l'ouverture de session Windows sans Docker.
Pour toute l'infrastructure (core + harness/backends lourds optionnels), voir Docker / compose ci-dessous.
Outils système optionnels (utilisés automatiquement lorsqu'ils sont présents — préinstallés dans l'image Docker core) :
| Outil | Rôle |
|---|---|
c2patool | Inspecter les manifestes C2PA |
exiftool | Suppression des métadonnées résiduelles (notamment PDF) |
qpdf | Reconstruction structurelle PDF — requis pour une véritable suppression PDF (voir ci-dessous) |
Les scripts core nécessitent uniquement la bibliothèque standard Python 3.10+. Les appels de modèle Layer B sont optionnels.
Utilisation rapide (scripts)```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
### Les outils de texte refusent les entrées binaires
`inspect_text.py`, `clean_text.py` et `rewrite_text.py` opèrent sur du texte. Pointés
vers un `.docx`, `.pdf` ou une image, ils décodaient auparavant les octets compressés et rapportaient
les codepoints qui en résultaient — du bruit qui suit la compression, pas le
contenu — et `clean_text.py` réécrivait ensuite ces octets altérés, détruisant le
fichier. Ils refusent désormais les entrées binaires et nomment l'outil qui les prend en charge :```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.
La détection repose sur le nombre magique et un ratio d'octets de contrôle, de sorte que le texte dans des encodages autres que UTF-8 continue de fonctionner. --force-text le remplace partout.
Les formats non reconnus ne sont jamais nettoyés automatiquement
classify() étiquette les octets qui ne correspondent à aucun format de texte, d'image ou de conteneur pris en charge comme unknown — il ne retombe plus sur « text ». En mode automatique, clean_file.py refuse ces fichiers (sortie 2, aucune sortie écrite) au lieu de les décoder en UTF-8 et de réécrire des octets altérés ; --as text ou --force-text sont les opt-in explicites. inspect_file.py signale le fichier comme unknown (sortie 0), et le service HTTP répond à /inspect avec kind: "unknown" mais rejette /clean des formats inconnus (400 — envoyez un nom de fichier avec une extension connue, par ex. notes.txt).
Service HTTP
La même mécanique fonctionne comme un service HTTP stdlib (service/scripts/server.py) — l'interface utilisée par le skill et la façon dont toute application web peut s'intégrer sans vendoring :
| Méthode | Chemin | Corps | Retourne |
|---|---|---|---|
| GET | /health | — | {"ok": true, "version": ...} |
| GET | /capabilities | — | outils / backends optionnels utilisables (chaque outil est sondé par version, pas seulement trouvé sur PATH) |
| GET | /openapi.json | — | spécification OpenAPI 3.0.3 générée dynamiquement |
| 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": {...}} ou {"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", ...}}, ...]} |
Les endpoints batch bouclent le même pipeline par fichier que /inspect, /detect, /clean et /watermark, plafonné à WATERMARKS_MAX_BATCH_FILES fichiers par requête (50 par défaut). Une entrée malformée (base64 invalide, option inconnue, format non reconnu) se manifeste par "ok": false pour cette entrée avec une chaîne "error" — elle n'interrompt jamais le reste du 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"}"
Le service effectue le routage par extension de fichier puis par magic bytes, ainsi le texte / l'image / le conteneur sont détectés automatiquement. Définissez `WATERMARKS_SERVER_API_KEY` pour exiger `Authorization: Bearer <key>` sur chaque requête. Liaison loopback uniquement par défaut (`--host` pour remplacer) ; destiné à un réseau de confiance.
### Détection de filigrane (`/detect` et `detect_before` / `detect_after`)
La détection est une étape distincte du nettoyage — le service n'appelle jamais les
API des fournisseurs sauf si vous le lui demandez :
- **`POST /detect`** exécute les détecteurs de filigrane configurés sur un fichier.
Texte → détecteurs de fournisseurs + stylométrie ; image → score pixel SynthID.
- **`/inspect`** accepte un flag opt-in `"detect": true` qui ajoute
les résultats des détecteurs au rapport texte (et peut basculer `suspicious`).
- **`/clean`** accepte les options `"detect_before"` / `"detect_after"` pour
évaluer l'entrée et la sortie nettoyée, afin de mesurer ce qu'un nettoyage
a réellement changé.
- **`/clean`** exécute la réécriture de texte Layer B après Layer A **par défaut** (c'est
une étape obligatoire pour le texte). Une option **`"strategy"`** (une liste ordonnée
`tactic@intensity`, par ex. `"[email protected],[email protected]"`) remplace la
valeur par défaut du fichier de configuration de stratégie (voir ci-dessous). Lorsque le backend/modèle de réécriture
pour une étape n'est pas configuré, `/clean` renvoie un 400.
Détecteurs de texte (voir `/capabilities` → `text_detectors`) :
Détecteurs de texte (voir `/capabilities` → `text_detectors`) :
| Détecteur | Activé par | Notes |
| --- | --- | --- |
| `markllm` | `MARKLLM_DIR` (checkout hôte) | Harnais de recherche (schémas KGW / SynthID), même-config uniquement — pas un oracle de fournisseur. |
| `gumbel` | `WATERMARKS_GUMBEL_KEY` | Rejeu sans modèle à clé identique du schéma keyed-Gumbel (Aaronson EXP) (voir `detect_gumbel.py`), stdlib uniquement — moteurs auto-hébergés tels que arbi-serve ; clé identique uniquement, pas un oracle de fournisseur. |
| `claude-text` | — (placeholder) | Anthropic a annoncé une API de détection de filigrane ; cette interface s'activera lorsqu'elle sera disponible. |
Scoring d'image : lorsque `WATERMARKS_SYNTHID_SCORER_URL` est défini, le service
évalue les images via le sidecar `wr-synthid-score` (profil heavy) ; avec un
`REVERSE_SYNTHID_DIR` local, il utilise directement le checkout. La détection est
fail-soft : les détecteurs non configurés, expirés ou en erreur rapportent
`{"available": false, "error": ...}` et ne bloquent jamais le nettoyage.
### Génération de filigrane (`/watermark` et `/watermark/batch`)
Génère du texte filigrané pour l'évaluation de benchmark et les tests aller-retour.
Lorsque `WATERMARKS_SYNTHID_TEXT_URL` est défini, le service délègue la génération au
sidecar `wr-synthid-text` (profil harness) ; avec un `MARKLLM_DIR` local, il utilise le
checkout directement. Comme la détection, la génération est fail-soft : un générateur non configuré
rapporte `{"ok": false, "error": ...}`.
## Docker / compose
Images publiées (GHCR) :
| Tag d'image | Contenu | Publiée ? |
| --- | --- | --- |
| `ghcr.io/guillaumemeyer/watermarks-remover:<tag>` / `:latest` | Service HTTP principal + tous les cleaners + exiftool / qpdf / c2patool | Oui |
| `…:markllm-<tag>` / `:markllm-latest` | Harnais de filigrane texte MarkLLM (upstream Apache-2.0) | Oui |
| `…:markdiffusion-<tag>` / `:markdiffusion-latest` | Harnais d'image MarkDiffusion (upstream Apache-2.0) | Oui |
| `watermarks-remover-ctrlregen:local` | Suppression de pixels CtrlRegen — **jamais publiée** (`noai-watermark` ne fournit aucune LICENSE) | Build local uniquement |
| `watermarks-remover-synthid-scorer:local` | Scorer reverse-SynthID — **jamais publiée** (licence de recherche non commerciale) | Build local uniquement (scorer CLI + sidecar HTTP `wr-synthid-score` optionnel sous le profil `heavy`) |
Construire et exécuter le service principal :```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
Mise en route de l'infrastructure complète :```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
La stack compose mappe le service principal sur `127.0.0.1:8765`. Les services persistants s'exécutent en tant que démons en arrière-plan (`wr-core` et le sidecar `wr-synthid-text` sous le profil harness). Les services harness/heavy restants sont des CLI one-shot — invoquez-les avec `docker compose run --rm <service> …` lorsque vous avez besoin de vérification ou de travail sur les pixels.
Validez la stack en cours d'exécution (code de sortie uniquement, aucune sortie en cas de succès) :```bash
make compose-check # or: ./compose-check.sh
Vérifie wr-core via GET /health et exécute chaque service harness/heavy avec --help, en exigeant un code de sortie 0.
Configuration (variables d'environnement pour docker compose)
Le nettoyage de texte nécessite la configuration de la Layer B — la réécriture de la Layer B est une
étape obligatoire pour POST /clean sur du texte, donc le service core a besoin que le backend de réécriture
soit configuré, sinon le nettoyage de texte renvoie HTTP 400. Le nettoyage des métadonnées d'images/conteneurs
fonctionne immédiatement. Pour le texte, vous devez configurer les dépendances de la stratégie Layer B :
transformers + roberta-large (pour l'étape mlm par défaut) et
la configuration LLM WATERMARKS_REWRITE_* (pour l'étape 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"}"
Les langues dont la typographie repose sur une espace insécable (français `« … »`, l'espace avant `; : ! ?`) doivent passer `"options": {"normalize_spaces": false}`, l'équivalent HTTP de `clean_text.py --no-normalize-spaces`. Les porteurs invisibles sont toujours supprimés ; seule la réécriture des espaces est ignorée.
Tout le reste est facultatif et réside dans un fichier `.env` à la racine du dépôt. `docker compose` **charge automatiquement `.env`** et y interpole les références `${VAR}` de `compose.yaml` (les exports du shell ont la priorité sur `.env` si les deux sont définis).```bash
cp .env.example .env # then edit
docker compose up -d # picks up .env automatically
.env est gitignored (refus par défaut) — ne jamais le commiter. Pour les exécutions CLI côté hôte (rewrite_text.py, la skill), exportez le même fichier dans l'environnement :```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`) | Require `Authorization: Bearer <key>` on the HTTP API |
| `WATERMARKS_GEMINI_*` | — | Removed Aug 2026: Google retired SynthID text watermarking on the API (see `vendor-notes.md`) |
| `WATERMARKS_SYNTHID_SCORER_URL` | `wr-core` | Point core at the `wr-synthid-score` sidecar for SynthID image scoring (e.g. `http://wr-synthid-score:8766` under the heavy profile) |
| `WATERMARKS_SYNTHID_SCORER_API_KEY` | `wr-core` + `wr-synthid-score` | Shared bearer key for the scorer sidecar (empty = no auth) |
| `WATERMARKS_SYNTHID_TEXT_URL` | `wr-core` | Point core at the `wr-synthid-text` sidecar for SynthID text watermarking (e.g. `http://wr-synthid-text:8767` under the harness profile) |
| `WATERMARKS_SYNTHID_TEXT_API_KEY` | `wr-core` + `wr-synthid-text` | Shared bearer key for the text watermark sidecar (empty = no auth) |
| `WATERMARKS_SYNTHID_TEXT_TIMEOUT` | `wr-core` | Seconds to wait for the `wr-synthid-text` sidecar (default 120) |
| `WATERMARKS_MARKLLM_SCHEME` | `text_detectors.py` (host) | MarkLLM scheme for `/detect`: `kgw` (default) / `synthid` |
| `HF_TOKEN` | harness/heavy services | Hugging Face token for gated models |
| `WATERMARKS_SERVICE_URL` | client only (skill / curl) | Where to reach the service; default `http://127.0.0.1:8765` |
| `WATERMARKS_REWRITE_BACKEND` | `rewrite_text.py` hook | `print-prompt` (default) / `ollama` / `openai-compatible` |
| `WATERMARKS_REWRITE_MODEL` | `rewrite_text.py` hook | Model name (e.g. `deepseek-v4-flash`) |
| `WATERMARKS_REWRITE_BASE_URL` | `rewrite_text.py` hook | API base (e.g. `https://api.deepseek.com`) |
| `WATERMARKS_REWRITE_API_KEY` | `rewrite_text.py` hook | API key — env only, never on argv |
| `WATERMARKS_REWRITE_ALLOW_REMOTE` | `rewrite_text.py` hook | `1` to allow non-loopback endpoints |
| `WATERMARKS_REWRITE_REASONING_EFFORT` | `rewrite_text.py` hook | `none` (default) / `low` / `medium` / `high` / `off` |
| `WATERMARKS_CLEAN_STRATEGY_FILE` | `server.py` `/clean` | Path to the Layer B strategy config JSON (default `config/clean_strategy.json`) |
| `WATERMARKS_GUMBEL_KEY` | `detect_gumbel.py` / `text_detectors.py` | Secret key for keyed-Gumbel (EXP) same-key replay (e.g. `0x…`); preferred over argv — never logged |
**La couche B est requise pour le nettoyage de texte.** `/clean` applique toujours la stratégie par défaut (depuis `config/clean_strategy.json`, `{"default_strategy": "[email protected],[email protected]"}`) à un fichier texte après la couche A, sauf si la requête transmet sa propre option `"strategy"` (une liste ordonnée `tactic@intensity`). Une étape de stratégie est `tactic@intensity` ; l'étape `mlm` nécessite `transformers` + `roberta-large`, et toute étape LLM (`paraphrase`, `humanize`, …) nécessite la configuration `WATERMARKS_REWRITE_*`. Si le backend/modèle requis n'est pas configuré — ou si aucune stratégie n'est disponible — `/clean` **rejette la requête avec un code 400**. Priorité pour le chemin de configuration : flag CLI `--strategy-config` > variable d'environnement `WATERMARKS_CLEAN_STRATEGY_FILE` > la valeur par défaut `config/clean_strategy.json`.
Les images sont publiées automatiquement sur les tags `v*` via [`.github/workflows/release-images.yml`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/.github/workflows/release-images.yml).
## Notation SynthID optionnelle par pixel
`inspect_image.py` et `clean_image.py` peuvent rapporter un score de confiance SynthID dans le domaine des pixels lorsqu'un checkout externe de
[`aloshdenny/reverse-SynthID`](https://github.com/aloshdenny/reverse-SynthID)
est disponible. Le scorer n'est **pas inclus** : il est chargé à l'exécution depuis votre checkout, et son code reste sous la licence de recherche non commerciale du projet amont.
### Option 1 : bootstrap en une commande (sans 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 accepte --dir PATH, --ref REF et --full (installe le
requirements.txt upstream complet, ce qui ajoute torch/diffusers pour le
contournement du VAE upstream que ce projet n'utilise pas).
Sous Windows, utilisez setup_synthid.ps1 (-Dir, -Ref, -Full), qui crée le
venv dans .venv\Scripts\ — la disposition que image_meta.py recherche déjà
lorsque os.name == "nt".
Option 2 : build Docker local```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'image est construite localement à partir de la source amont au moment du build. Elle n'est pas publiée, donc elle ne redistribue pas le code amont.
### Option 3 : sidecar de scoring HTTP (docker compose)
Sous le profil `heavy`, la stack compose exécute également le scorer en tant que sidecar HTTP (`wr-synthid-score`) afin que le **service core publié** puisse scorer les images avant/après nettoyage sans embarquer le code amont non commercial. Pointez `wr-core` vers celui-ci et partagez une clé bearer (voir `.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
Puis POST /clean avec {"options": {"detect_before": true, "detect_after": true}} renvoie synthid_before / synthid_after dans le
rapport, et POST /detect sur une image renvoie le score SynthID. Fail-soft :
si le sidecar est arrêté ou non configuré, les rapports contiennent
{"available": false, "error": ...} et le nettoyage réussit quand même.
Le scoring V4 utilise artifacts/spectral_codebook_v4.npz du checkout upstream
(`220 MB). Il s'agit de détection/scoring uniquement — cela ne supprime pas les
watermarks pixel.
Suppression optionnelle des pixels CtrlRegen
Pour les watermarks d'image dans le domaine pixel (classe SynthID, StegaStamp, Tree-Ring,
StableSignature), un backend externe optionnel exécute le pipeline CtrlRegen
(ControlNet + DINOv2 IP-Adapter controllable regeneration). Le backend est
mertizci/noai-watermark, une
réimplémentation maintenue de la méthode ICLR 2025
CtrlRegen avec tiling automatique.
Le backend n'est pas inclus et ne fournit aucun fichier LICENSE, il est donc traité comme
all-rights-reserved : il est cloné à un commit épinglé et chargé à l'exécution.
Ses dépendances épinglées de l'époque de la recherche (requirements-ctrlregen.txt — par ex.
transformers==4.37.2, diffusers==0.27.2) comportent des avis publiés et
ne sont intentionnellement pas à jour, elles ne sont donc installées que dans le
venv dédié que ce script crée et jamais dans l'image de service principale ;
setup_ctrlregen.sh revérifie également le commit épinglé sur les checkouts
existants, pas seulement sur les clones frais.
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
Sur Windows, utilisez `setup_ctrlregen.ps1` (mêmes options que `-Dir`, `-Ref`, `-Python`) ;
le venv se trouve dans `.venv\Scripts\`, que `clean_image.py` résout déjà.
Il sonde les index de wheels PyTorch publiés et sélectionne le plus élevé égal ou
inférieur à la version CUDA affichée par `nvidia-smi` qui existe réellement — ce nombre
est le maximum pris en charge par le *pilote*, et les pilotes sont rétrocompatibles, donc un
pilote indiquant 13.1 (aucun `cu131` publié) installe `cu130`. En dessous de la capacité de calcul
7.5, il force `cu126`, le dernier index dont les wheels contiennent encore les kernels
Maxwell/Pascal/Volta. Il installe `torch` **et** `torchvision`
ensemble depuis cet index afin que l'installation des dépendances ne puisse pas les remplacer par des builds CPU
depuis PyPI, puis vérifie après l'installation que `torch.cuda.is_available()`
est vrai — si un GPU a été détecté mais que torch finit en CPU uniquement, le script avertit
bruyamment et se termine avec un code non nul au lieu de prétendre que l'installation a réussi.
### Depuis `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
Ordre des opérations : suppression des métadonnées d'abord, puis suppression des pixels CtrlRegen, puis
un score reverse-SynthID avant/après optionnel (lorsque REVERSE_SYNTHID_DIR est
également défini).
L'intensité est conservatrice par défaut (--ctrlregen-intensity 0.25), car
une intensité plus élevée supprime davantage de filigrane mais régénère davantage l'image.
Préréglages documentés : 0.15 minimal / 0.25 par défaut / 0.35 équilibré /
0.5 agressif / 0.7 max (la valeur par défaut du backend est 0.5). --ctrlregen-steps
est par défaut à 50 (étapes de débruitage effectives ≈ steps × intensity).
Taille d'image (limite native 512×512)
CtrlRegen est un ControlNet Stable Diffusion 1.5 en 512×512. Le backend résout cela pour des entrées arbitraires, donc aucun tuilage supplémentaire n'est exposé ici :
- ≤512 px : passe unique — recadrage centré/redimensionnement à 512, régénération, redimensionnement inverse.
- >512 px : tuilage automatique avec chevauchement (tuiles de 512 px, chevauchement de 192 px), largeur/hauteur alignées sur des multiples de 8, puis coutures fusionnées par cosinus.
- Dans les deux cas : la sortie est redimensionnée à la taille d'origine et les couleurs sont harmonisées avec l'image d'origine.
Les images très volumineuses (par ex. 4K) produisent de nombreuses tuiles, donc les exécutions évoluent avec le nombre de tuiles (plus lentes et VRAM plus élevée). Pré-réduisez les grandes entrées lorsque c'est possible ; la taille des tuiles et le chevauchement sont codés en dur en amont et ne sont pas exposés comme options.
Calcul, modèles à accès restreint et vérification
Attendez-vous à environ 10 Go de téléchargements de modèles ; un GPU est fortement recommandé et les exécutions sur CPU
sont lentes. Certains modèles en amont sont à accès restreint, donc exportez HF_TOKEN (env uniquement —
jamais en argv). clean_ctrlregen.py refuse d'installer automatiquement les dépendances ; exécutez
setup_ctrlregen.sh d'abord.
Il n'existe pas de détecteur local pour StegaStamp/Tree-Ring/StableSignature, donc le
seul signal local est le score reverse-SynthID (un substitut). Lorsqu'il est disponible,
clean_image.py --remove-pixel ctrlregen rapporte ce score avant/après ; la
vérification officielle Google SynthID reste 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
## Vérification optionnelle du filigrane textuel MarkLLM
Pour des **expériences contrôlées**, un harnais externe optionnel encapsule
[`THU-BPM/MarkLLM`](https://github.com/THU-BPM/MarkLLM) (Apache-2.0) afin de
marquer d'un filigrane un texte de test et de le redétecter après une réécriture Layer B — par exemple, prouver qu'une marque KGW (Kirchenbauer, votre ligne « open-LLM ») ou SynthID-Text (ligne Gemini) disparaît sous votre réécriture. C'est un **harnais de vérification, pas un oracle** :
la détection MarkLLM n'est valide que par rapport à la *même* configuration de schéma + clés utilisées à
la génération, et elle ne peut pas certifier qu'un détecteur de fournisseur échouera.
Le backend n'est **pas fourni**. `setup_markllm.sh` clone le dépôt upstream à un commit épinglé, crée un venv et installe des dépendances épinglées (torch + transformers) ; le
modèle de scoring (par défaut `facebook/opt-1.3b`, Apache-2.0) est téléchargé depuis Hugging
Face au premier lancement.```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
Vérification autour d'une réécriture de couche B : passez --markllm-scheme à
rewrite_text.py (avec --markllm-dir), et il enregistre la détection MarkLLM
avant/après ainsi qu'un indicateur 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
**Réécriture itérative guidée par la détection :** La couche B réécrit désormais de manière itérative et
s'arrête dès qu'une tentative passe l'évaluation. Chaque cycle d'évaluation génère
`--candidates` variantes (par défaut **1**, `WATERMARKS_REWRITE_CANDIDATES`)
et `--max-loops` limite le nombre de cycles exécutés avant que la variante au meilleur effort ne soit
retournée (par défaut **1**, `WATERMARKS_REWRITE_LOOPS`). Chaque variante est un
appel de réécriture plus une évaluation, et un cycle se termine prématurément à la première tentative
que l'évaluateur signale comme non marquée — ainsi, augmenter `--max-loops` réessaie
de nouvelles variantes jusqu'à ce qu'une évaluation passe (une réécriture propre typique coûte une
tentative). L'évaluateur est choisi par priorité :
1. **MarkLLM** — détection de recherche à configuration identique, lorsque `--markllm-scheme` est
passé (avec `--markllm-dir`). Un emplacement de détecteur de fournisseur est réservé au-dessus de
MarkLLM pour le détecteur SynthID-text de Google, que Google a retiré de son API
en août 2026 — un futur point de terminaison de fournisseur pourra s'y brancher.
2. **divergence lexicale bigram-Jaccard** — lorsqu'aucun détecteur n'est configuré ; pas de
verdict de réussite/échec, donc chaque tentative est générée et celle qui diverge le plus lexicalement
est sélectionnée (le comportement d'origine).
`--json-stats` rapporte l'évaluateur, les tentatives effectuées, la réussite/l'échec, et les enregistrements par tentative :```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 détecteur non configuré, qui expire ou qui renvoie une erreur produit une
entrée "available": false avec une raison error et ne fait jamais échouer la
réécriture — cette tentative ne peut simplement pas passer, et la boucle se
rabat sur la sélection par divergence lexicale. Lorsque le maximum est épuisé
sans qu'une tentative n'ait passé, la tentative la moins watermarkée (score le
plus bas) est renvoyée au mieux avec une note.
Si le backend n'est pas configuré ou que ses dépendances sont manquantes, la réécriture se poursuit et le rapport indique que la vérification était indisponible. Un GPU est recommandé ; les exécutions sur CPU fonctionnent mais sont lentes, et le téléchargement du modèle représente quelques Go.
Réglages de durcissement :
--offlinesur l'adaptateur (ou toute exécution MarkLLM) charge le modèle de scoring uniquement depuis le cache Hugging Face — aucune sortie réseau ; échoue rapidement s'il n'est pas en cache. Le code distant personnalisé n'est jamais exécuté (letrust_remote_codede transformers n'est jamais activé).WATERMARKS_MARKLLM_RLIMIT_AS=<bytes>(env, POSIX) applique une limite d'espace d'adressage au sous-processus du détecteur MarkLLM. Désactivé par défaut car torch/CUDA nécessite généralement de grands espaces d'adressage.- Les fichiers de configuration sont plafonnés à 1 MiB ; le checkout upstream et l'image de base sont épinglés par 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
### Vérification à clé identique de Keyed-Gumbel (Aaronson EXP)
[Le rapport technique d'ARBI](https://arbicity.com/news/ai-text-watermarking-for-self-hosted-ai/) décrit
le filigrane de texte keyed-Gumbel (« exponentiel ») — désormais intégré au moteur
open-source arbi-serve (`ARBI_WATERMARK_KEY`) — où le bruit de l'échantillonneur est dérivé
d'un hachage à clé de la fenêtre de contexte des 4 derniers tokens. La détection est une
**relecture sans modèle** : recalculer `u = PRF(Hash(key, window), token)` à partir du
texte seul et tester la queue Gamma, ce qui ne nécessite ni GPU, ni modèle, ni logits.
Ce dépôt fournit ce détecteur sous le nom de `detect_gumbel.py` (stdlib uniquement ; la p-value
est l'identité exacte de la somme de Poisson pour une forme Gamma entière) :```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
Même mise en garde d'honnêteté que pour MarkLLM : il s'agit d'un rejeu à clé identique — valable uniquement contre la même clé, le même tokenizer et la même disposition PRF que ceux utilisés lors de la génération, et un résultat négatif n'établit rien. La disposition HMAC-SHA256 ici est une instanciation auditable, non bit-compatible avec un noyau de moteur spécifique (voir la docstring du module pour ce qu'il faut adapter pour un rejeu exact).
Réécriture guidée par la détection : passez --gumbel-key à rewrite_text.py
(env : WATERMARKS_GUMBEL_KEY, préféré) et la boucle de réécriture itérative est
pilotée par le rejeu Gumbel à clé identique — la priorité de l'évaluateur devient gumbel >
MarkLLM > divergence lexicale — avec un rapport 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 clé n'apparaît jamais dans les stats ni les logs. Les opérateurs auto-hébergés qui détiennent la clé de leur moteur peuvent vérifier qu'une réécriture a effacé une marque Gumbel ; tous les autres traitent la Couche B comme du best-effort uniquement.
## Benchmark optionnel de suppression de SynthID-text
[`bench_synthid_text.py`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/service/scripts/bench_synthid_text.py) mesure avec quelle efficacité une réécriture de Couche B efface les watermarks de la classe SynthID-text et à quel coût. Il génère des échantillons watermarkés + non watermarkés avec le schéma SynthID de MarkLLM (détection à configuration identique, avec garde-fou de cohérence), exécute vos variantes de réécriture (tactique × nombre maximal de tentatives de réécriture ; la boucle s'arrête tôt en cas de succès) plus des contrôles (sans suppression, Couche A uniquement, vérification optionnelle de re-marquage), et écrit un `report.md` / `results.json` / `results.csv` partageable. Guide complet :
[`docs/synthid-text-benchmark.md`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/docs/synthid-text-benchmark.md).
Nécessite un checkout MarkLLM (`setup_markllm.sh` / `MARKLLM_DIR`) et un backend de réécriture. **Le modèle de réécriture est un LLM que vous configurez** — le même backend `rewrite_text.py` que celui utilisé par le skill. Le `facebook/opt-1.3b` par défaut de MarkLLM (`--markllm-model`) n'est que le générateur/détecteur de watermark ; il ne réécrit jamais. Configurez le modèle de réécriture via des variables d'environnement ou des flags du benchmark (ils reflètent le
[tableau de configuration](#configuration-env-vars-for-docker-compose) ci-dessus) :
| Variable d'env | Flag du benchmark | Défaut | Signification |
| --- | --- | --- | --- |
| `WATERMARKS_REWRITE_BACKEND` | `--rewrite-backend` | `ollama` | `ollama` ou `openai-compatible` |
| `WATERMARKS_REWRITE_MODEL` | `--rewrite-model` | *(requis)* | Le LLM qui effectue la réécriture (par ex. `llama3.2`, `deepseek-v4-flash`) |
| `WATERMARKS_REWRITE_BASE_URL` | `--rewrite-base-url` | `http://127.0.0.1:11434` | Point de terminaison ; le défaut Ollama est en loopback |
| `WATERMARKS_REWRITE_API_KEY` | `--rewrite-api-key` | — | Clé API (env uniquement dans le processus enfant, jamais en argv) |
| `WATERMARKS_REWRITE_ALLOW_REMOTE=1` | `--rewrite-allow-remote` | désactivé | Requis pour envoyer du contenu vers des points de terminaison 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
Utilisez un modèle non-origine pour la réécriture (ne réécrivez pas avec le même modèle filigrané qui a généré le texte), sinon la réécriture peut réapposer le filigrane sur la sortie ; --restamp-control mesure cela.
Harnais optionnel de filigrane d'image MarkDiffusion
Pour des expériences contrôlées sur des images, un harnais externe optionnel encapsule
THU-BPM/MarkDiffusion (Apache-2.0),
une boîte à outils de filigrane génératif pour les modèles de diffusion latente (elle intègre des marques
— elle ne les supprime pas). Nous l'utilisons pour trois choses :
- Harnais de vérification (comme MarkLLM, mais pour les images) : appliquer un filigrane à une image de test avec un schéma, exécuter la suppression, puis re-détecter avec la même configuration de schéma — par exemple prouver qu'une marque de classe Tree-Ring s'efface sous votre pipeline. C'est un harnais de vérification, pas un oracle : la détection nécessite le modèle générateur (et les clés pour les schémas basés sur clé), donc il ne peut pas certifier qu'un détecteur de fournisseur échouera sur une image arbitraire.
- Moteur optionnel de suppression de pixels : son attaque de régénération
DiffusionPurificationest exposée commeclean_image.py --remove-pixel diffusion, une alternative à CtrlRegen. C'est une régénération aveugle (sans conditionnement ControlNet), donc elle dérive davantage le contenu de l'image que CtrlRegen — intensité par défaut conservatrice (0.3), traitée comme un repli/comparaison, jamais une garantie. - Détecteur local à schéma identique pour les marques de classe Tree-Ring, comblant partiellement le manque de « détecteur local pour StegaStamp/Tree-Ring/StableSignature » (il couvre Tree-Ring/Ring-ID/Gaussian-Shading etc., pas StegaStamp / StableSignature / SynthID-media).
Le backend n'est pas fourni. setup_markdiffusion.sh crée un venv et
installe markdiffusion==1.0.2 depuis PyPI (épinglé), avec torch installé depuis
l'index de plateforme approprié ; --checkout installe à la place un clone éditable à un commit épinglé. Le modèle Stable Diffusion (par défaut
huanzi05/stable-diffusion-2-1-base) se télécharge depuis Hugging Face au premier lancement.```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
Ou exécutez la purification dans le cadre du pipeline d'images normal :```bash
MARKDIFFUSION_DIR=~/markdiffusion \
~/markdiffusion/.venv/bin/python "$SCRIPTS/clean_image.py" shot.png \
-o shot.cleaned.png --remove-pixel diffusion
Les paramètres de durcissement reflètent le harnais MarkLLM : --offline charge le modèle uniquement depuis le cache Hugging Face (aucune sortie réseau, aucun code distant), HF_TOKEN est uniquement en variable d'environnement (jamais en argument), les configurations d'algorithme sont plafonnées à 1 MiB, et le sous-processus reçoit les mêmes limites de ressources supérieures que 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'image installe un torch CPU ; les utilisateurs de CUDA doivent exécuter `setup_markdiffusion.sh` sur l'hôte à la place. Les téléchargements de modèles passent toujours par le hub HF au premier lancement.
## Matrice de couverture
| Canal | Claude | Gemini/SynthID | OpenAI | Open-LLM |
| --- | --- | --- | --- | --- |
| Texte Unicode / basé sur les modifications | Couche A | Couche A | Couche A | Couche A |
| **Texte d'échantillonnage statistique** | Couche B au mieux (couture Claude lorsque l'API de détection d'Anthropic sera disponible) | Couche B au mieux (+ harnais MarkLLM à configuration identique ; Google a retiré le détecteur du fournisseur en août 2026) | Couche B si présente | Couche B au mieux + harnais MarkLLM optionnel |
| C2PA / métadonnées de fichier | Oui (formats listés) | Oui lorsqu'elles sont présentes | Oui lorsqu'elles sont présentes | Oui lorsqu'elles sont présentes |
| Marques d'image pixel | Hors périmètre | Score SynthID optionnel + suppression CtrlRegen (externe) ; détection MarkDiffusion à schéma identique optionnelle + suppression DiffusionPurification (externe) | Hors périmètre | Suppression CtrlRegen / MarkDiffusion optionnelle (externe) |
| Portes dérobées d'entraînement | Hors périmètre | Hors périmètre | Hors périmètre | Hors périmètre |
Détails : [`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).
---
## Comment fonctionne le marquage de texte (bref)
Les filigranes modernes des LLM cachent souvent un signal dans **quels tokens sont choisis** (biais génératif / d'échantillonnage), et pas seulement dans des caractères invisibles. Les schémas basés sur les modifications injectent des règles Unicode ou de synonymes. Les schémas de fichiers attachent **C2PA** ou des métadonnées de générateur.
- **Couche A** supprime les porteurs Unicode basés sur les modifications (testable).
- **Couche B** attaque les filigranes d'échantillonnage via une réécriture lourde (au mieux ; attaques standard de la littérature telles que la paraphrase / la rétro-traduction).
- **Les nettoyeurs de fichiers** suppriment C2PA/XMP/props des conteneurs pris en charge.
Tant que les fournisseurs ne livrent pas de détecteurs et de clés publics, **aucun outil ne peut honnêtement certifier** « ceci échoue au contrôle officiel ». Les rapports doivent séparer le travail vérifiable du travail au mieux.
Préférez un modèle **non-origine** pour la Couche B (ne réécrivez pas du texte Claude avec Claude si vous cherchez à éviter un re-marquage).
---
## Avertissement : ce que coûte la suppression d'un filigrane de texte
Les filigranes de texte vivent dans **la formulation elle-même** : le signal est réparti sur les choix de tokens, donc presque chaque phrase en porte un peu. Deux conséquences en découlent, et c'est pourquoi la Couche B est honnêtement décrite comme *au mieux* plutôt que comme une gomme magique.
1. **Supprimer signifie reformuler, pas restructurer.** Mélanger les paragraphes, changer les titres ou faire des retouches légères ne déplace guère le signal. Retirer une marque statistique exige de réécrire une fraction substantielle du texte — phrase par phrase, pas section par section.
2. **La reformulation dégrade la copie.** Toute réécriture remplace les choix de mots originaux par ceux du modèle de réécriture, ce qui aplatit le ton, la voix et la précision. Sur une copie de production (SEO, marketing, travail client), cette dégradation est réelle et souvent visible pour ceux qui se soucient le plus de l'écriture. C'est comme prendre du texte d'un modèle de premier plan et demander à un modèle moins capable de le réécrire de zéro : le résultat ne peut pas dépasser le plafond du modèle de réécriture.
Ce qui mène à l'honnête question du bouclage :
> Si le plan est de toute façon de réécrire le texte avec un modèle moins cher, pourquoi payer pour un modèle premium au départ ? Générer directement avec le modèle moins cher est plus simple, moins cher, et produit le même — ou un meilleur — résultat final.
La Couche B a du sens lorsque vous voulez spécifiquement la **réflexion et la rédaction** du modèle premium et acceptez une passe de réécriture pour satisfaire une exigence d'hygiène ou de confidentialité — pas comme une voie économique vers un texte sans marque.
**Quand sauter la Couche B :**
- **La qualité compte plus que l'hygiène :** utilisez le chemin sans perte — nettoyage Unicode de la Couche A plus les nettoyeurs de métadonnées de fichiers — et gardez la prose originale.
- **Réécriture de toute façon :** utilisez un modèle **non-origine** (réécrire avec le modèle d'origine peut re-marquer le texte), et rappelez-vous qu'un risque résiduel demeure — aucun outil ne peut certifier qu'un détecteur de fournisseur échouera.
---
## Formats de fichiers
| Format | Inspecter | Nettoyer |
| --- | --- | --- |
| PNG / JPEG / WebP | Chunks C2PA / APP11 / RIFF `C2PA`, indices XMP IA | Supprimer les segments de métadonnées |
| AVIF / HEIC | Boîtes ISOBMFF `jumb` / XMP `uuid` | Supprimer les boîtes |
| BMP | Octets non-image en fin de fichier (aucun canal standardisé) | Tronquer les métadonnées finales, corriger le champ de taille de fichier |
| GIF | Extensions d'application Comment / XMP | Supprimer commentaire & XMP, conserver la boucle `NETSCAPE2.0` |
| TIFF (classique + BigTIFF) | Tags IFD : XMP, EXIF, GPS, IPTC, MakerNote | Supprimer les tags, mettre à zéro les charges utiles, conserver les strips |
| SVG | `<metadata>`, XMP | Supprimer les blocs |
| PDF | Octets/XMP + outils optionnels | **exiftool** puis **qpdf**, puis **ghostscript** pour les métadonnées à l'intérieur des images intégrées ; chaque outil manquant dégrade une couche différente (suppression du document, réécriture structurelle, images intégrées) |
| DOCX | docProps / customXml | Nettoyer les props, supprimer customXml |
| EPUB | Métadonnées OPF, meta/JSON-LD XHTML, médias intégrés | Nettoyer l'OPF, supprimer les meta XHTML, nettoyer les médias + Couche A (ignore les parties chiffrées) |
| ODT | meta.xml | Supprimer les métadonnées de générateur / de type IA |
| HTML | meta, JSON-LD, data-ai* | Supprimer les tags/attrs |
| Markdown | Clés IA du frontmatter YAML | Supprimer les clés + corps Couche A |
| MP4 / MOV / M4A / M4V | Boîtes ISOBMFF `jumb`/`uuid` (même mécanisme que AVIF/HEIC) + tags de générateur `moov/udta` | Supprimer les boîtes |
| WAV | Chunks RIFF `C2PA` / `LIST INFO`, chunk `id3\x20` intégré | Supprimer les chunks |
| MP3 | Frames ID3v2 (v2.3/v2.4 par frame ; v2.2 tag entier) | Supprimer les frames correspondantes ou le tag entier |
| FLAC | Manifeste C2PA dans une frame ID3v2 `GEOB` | Supprimer la frame correspondante ou le tag ID3v2 entier |
La prise en charge de FLAC couvre le porteur ID3v2 standardisé de C2PA. Les blocs de métadonnées FLAC natifs, les Vorbis Comments et les filigranes du domaine de la forme d'onde sont laissés intacts.
#### Pourquoi PDF a besoin de qpdf, pas seulement d'exiftool
ExifTool écrit les PDF **de manière incrémentale**. `exiftool -all=` ajoute un bloc `%BeginExifToolUpdate` qui libère l'objet Info et supprime `/Info` du trailer — mais les octets de métadonnées originaux restent dans le fichier mot pour mot, et exiftool lui-même peut annuler la modification avec `-PDF-update:all=`. La commande se termine avec le code `0`, les visionneuses n'affichent aucune métadonnée, et le fichier devient *plus gros*, ce qui est l'indice révélateur.
Pour un outil de suppression de provenance, c'est une fuite silencieuse, donc `clean_pdf` fait suivre la passe exiftool de `qpdf --linearize`, qui re-sérialise le document à partir de son graphe d'objets et supprime les objets désormais non référencés. Sans `qpdf` installé, le nettoyage s'exécute quand même, mais il le signale :```
warning: exiftool PDF edits are incremental — the original metadata bytes
remain recoverable; install qpdf for a structural rewrite
Pourquoi qpdf ne suffit pas pour les images à l'intérieur du PDF
Les deux passes ci-dessus agissent sur le document : le dictionnaire Info, le paquet XMP, le graphe d'objets. Aucune ne descend dans un XObject image, donc un scan ou un export Photoshop — une page qui est un grand JPEG — conserve tout ce que l'image transporte. Sur un vrai PDF exporté par Photoshop, cela laisse 27 balises en place après un nettoyage « réussi », dont IFD0:Software, les horodatages de capture et une vignette d'aperçu ; un manifeste C2PA attaché à la même image y survit également.
Ainsi, clean_pdf ajoute une troisième passe, deep_images, pilotée par pdfwrite de Ghostscript. Elle s'exécute en deux paliers et s'arrête dès que le fichier est propre :
- Sans perte.
pdfwriteavec pass-through reconstruit le document à partir du graphe d'objets tout en copiant les données d'image compressées octet par octet — vérifié en hachant les flux avant et après. Cela élimine tout ce que le PDF enveloppait autour de l'image. Le pass-through couvre les codecs que Ghostscript prend en charge pour cela, JPEG (DCTDecode) et JPEG2000 (JPXDecode) ; les images Flate, CCITT et LZW sont décodées et ré-encodées, ce qui est sans perte en pratique pour ces codecs mais pas identique octet par octet.neverest l'option pour un document dont les flux doivent rester intacts. - Ré-encodage, uniquement sur preuve. Tout ce qui vit dans les segments APPn propres au JPEG — EXIF dans APP1, un manifeste C2PA dans APP11, les ressources Photoshop dans APP13 — voyage avec les octets auxquels il est attaché, donc le pass-through le préserve. Le palier 2 exécute la même passe avec le pass-through désactivé, et uniquement lorsque le palier 1 a manifestement laissé quelque chose derrière : un marqueur AI/C2PA dans n'importe quel mode, ou, sous
always, toute métadonnée APPn survivante. APP0 (JFIF) et APP2 (ICC) sont laissés tranquilles — le premier est structurel et le second décide comment les couleurs sont lues. Les pixels sont dépensés sur preuve, jamais sur soupçon.
deep_images accepte auto (par défaut : palier 1 uniquement lorsque des marqueurs ont survécu au strip du document, puis palier 2 s'ils survivent à cela), always (palier 1 pour chaque PDF, avec escalade vers le palier 2 pour l'EXIF d'appareil photo et d'éditeur également), lossless (palier 1 uniquement — ne jamais recompresser, et signaler tout ce qui survit via les champs habituels still_has_c2pa / post_findings) et never. Une valeur non reconnue est rejetée plutôt que traitée silencieusement comme auto. Le rapport indique quels paliers ont été exécutés via meta.deep_image_pass et meta.images_reencoded, et lorsque la passe est ignorée, il nomme l'option qui irait plus loin :```text
deep image pass not needed for AI/C2PA markers; pass deep_images="always"
to also clear non-AI EXIF inside images
Sans Ghostscript installé, le nettoyage s'exécute quand même et indique ce qu'il n'a pas pu atteindre :```text
warning: metadata inside embedded images left in place; install ghostscript
for the deep image pass
La suppression de filigrane dans le domaine pixel est désormais disponible en tant que backend CtrlRegen externe optionnel (voir ci-dessus) ; il s'agit d'un suppresseur régénératif, pas d'une garantie. Le soft binding C2PA (filigrane intégré au contenu qui peut relier à nouveau un manifeste Content Credentials distant après que les métadonnées ont été supprimées) reste hors périmètre. La suppression du C2PA fortement lié ne nettoie pas ces canaux.
Risque résiduel après un nettoyage
Cet outil signale les suppressions vérifiables (comptages Unicode, actions sur les métadonnées) et les réécritures de Couche B au mieux. Il ne peut pas certifier que les détecteurs des fournisseurs échoueront.
Pour vérifier vous-même les signaux résiduels (optionnel, externe) :
| Canal | Ce que nous supprimons | Ce qui peut subsister | Vérification externe (exemples) |
|---|---|---|---|
| C2PA / EXIF / XMP fortement lié | Oui | Marques soft-bound / pixel | c2patool, Content Credentials verify |
| Médias de classe SynthID | Suppression pixel optionnelle (CtrlRegen externe) ; score local sinon | Filigrane audio/vidéo ; filigrane pixel résiduel après suppression | Outils du fournisseur (par ex. Google SynthID / détecteur Vertex lorsqu'il est proposé) ; scoreur local optionnel reverse-SynthID |
| Texte statistique | Réécriture au mieux | Marques fortes après édition légère | Aucun détecteur universel public ; outils du fournisseur lorsqu'ils sont disponibles |
Contexte industriel à deux couches (C2PA + filigrane imperceptible) : guide de l'Institute of AI PM.
Détecteurs de filigrane
Vérificateurs fournis par les fournisseurs pour vérifier si un contenu porte des marques de provenance IA :
- Claude : Vérifier si un fichier a été créé avec Claude — lit les identifiants de contenu C2PA dans les images, vidéos et fichiers audio pour indiquer si Claude a participé à la production du fichier ; s'exécute dans le navigateur. L'API de détection de filigrane de texte de Claude est actuellement en aperçu privé.
- OpenAI : Vérifier le contenu généré par OpenAI — téléchargez une image ou un fichier audio et vérifiez la présence de signaux de provenance OpenAI (métadonnées C2PA et filigranes SynthID). Une API programmatique est également disponible.
- Google DeepMind : SynthID — la technologie de filigrane de Google pour les images, l'audio, le texte et la vidéo générés par IA, avec un aperçu de la façon dont les marques imperceptibles sont intégrées et détectées.
- Gemini : Vérifier les images, vidéos et fichiers audio générés par IA — le guide de Google pour vérifier les fichiers dans l'application Gemini à l'aide des filigranes SynthID et des Content Credentials, y compris les limites de téléchargement et comment lire les résultats.
Options de suppression (résumé)
| Option | Supprime | Remarques |
|---|---|---|
| Nettoyage Unicode (Couche A) | ZWSP, bidi, balises, espaces exotiques, … | Valeur par défaut sûre pour le texte |
| Réécriture (Couche B) | Marques statistiques de tokens (au mieux) | Toujours proposée par la compétence ; coûte en style — voir Avertissement |
| Suppression conteneur/métadonnées | Provenance du fichier | Voir le tableau des formats |
| Suppression pixel CtrlRegen (optionnelle) | Marques d'image dans le domaine pixel (classe SynthID, StegaStamp, Tree-Ring, StableSignature) | Backend externe ; calcul intensif ; intensité par défaut conservatrice |
| Suppression pixel DiffusionPurification (optionnelle) | Marques d'image dans le domaine pixel (classe Tree-Ring) | Backend MarkDiffusion ; régénération aveugle (plus de dérive que CtrlRegen) ; intensité par défaut conservatrice |
| Modèles locaux à poids ouverts | Éviter le re-marquage avec le modèle d'origine | Alternative opérationnelle |
Matrice : skills/remove-ai-marks/references/removal-matrix.md.
Éthique et avertissement
Voir skills/remove-ai-marks/references/ethics.md. Pour la confidentialité et la recherche sur votre contenu — pas pour la fraude académique ou de fausses affirmations de contenu « écrit par un humain ».
Utilisation responsable : Ce projet est destiné au contenu que vous possédez ou que vous êtes autorisé à traiter. Les utilisateurs doivent respecter les réglementations locales et l'utiliser de manière responsable. Les développeurs déclinent toute responsabilité en cas d'utilisation abusive potentielle par les utilisateurs.
Écosystème
Projets tiers qui encapsulent ou complètent ce dépôt, listés à des fins de découvrabilité uniquement. Ils ne sont ni maintenus, ni approuvés, ni pris en charge par ce projet. Ce projet n'examine pas leur code, ne se porte pas garant de leur comportement ou de leurs garanties, et n'assume aucune responsabilité pour tout ce que vous installez ou exécutez à partir de cette liste. Chaque projet est régi par sa propre licence, ses propres mainteneurs et sa propre documentation — lisez-les avant de l'utiliser.
MetaClean — interface graphique de bureau
MetaClean est une application de bureau indépendante Rust/Tauri sous licence MIT (Windows, macOS, Linux) fournissant une interface graphique native packagée pour le nettoyage de métadonnées par glisser-déposer, avec une zone de notification système et une intégration à l'Explorateur. C'est une base de code distincte : elle n'appelle pas le service Python de ce dépôt, et ses formats pris en charge et ses garanties de nettoyage diffèrent de ceux de ce projet. Consultez son README pour plus de détails.
unmark-web — interface web dans le navigateur
unmark-web est un client web statique indépendant sous licence MIT. Il supprime les marques Unicode invisibles du texte et retire les métadonnées de provenance des images entièrement dans le navigateur, et peut éventuellement appeler le service HTTP de ce dépôt pour les formats qu'il ne gère pas localement. C'est une base de code distincte et elle n'est pas affiliée à ce projet ; consultez son README pour la portée et les limites.
DropMarks — interface graphique macOS
DropMarks est une application macOS SwiftUI indépendante sous licence MIT. Elle appelle les scripts inspect_file.py / clean_file.py de ce dépôt (et éventuellement rewrite_text.py) via un instantané vendu de ces scripts stdlib. C'est une base de code distincte et elle n'est pas affiliée à ce projet ; consultez son README pour la portée et les limites.
Ajouter un projet
Pour enregistrer un projet ici, ouvrez une PR ajoutant une courte entrée — nom du projet, ce qu'il encapsule ou ajoute, et un lien vers son propre dépôt. Gardez les entrées brèves et factuelles ; ne revendiquez pas de compatibilité avec ce projet, ni d'approbation par celui-ci. Un projet listé doit s'appuyer sur ce dépôt ou l'intégrer — par exemple, en appelant son service ou en réutilisant son moteur de détection — plutôt que de simplement traiter le même problème de manière indépendante. Veuillez éviter les noms qui commencent par ou ressemblent étroitement à watermarks-remover — les noms similaires rendent difficile de distinguer quel projet est lequel.
Hook pre-commit
Le contrôle CI existe déjà (export SARIF de audit_dir.py, voir le contexte de la matrice de couverture) — les hooks pre-commit ci-dessous interceptent la même classe de problème plus tôt, avant même qu'un fichier marqué ne soit commité. Les deux encapsulent les CLI existantes (audit_dir.py / clean_file.py) — aucune logique de détection séparée.```yaml
.pre-commit-config.yaml
repos:
- repo: https://github.com/guillaumemeyer/watermarks-remover
rev: v0.5.0 # pin to a tag/commit
hooks:
- id: watermarks-remover-check # fails the commit if marks are found
- id: watermarks-remover-clean # opt-in: cleans staged files in place instead
`watermarks-remover-check` fait échouer le commit et liste les résultats ; `watermarks-remover-clean` est optionnel et réécrit les fichiers indexés sur place (sort avec le code 1 pour que vous examiniez le diff et réindexiez — la même convention que les hooks de correction automatique comme `ruff --fix`). Lorsque le nettoyeur ne peut pas traiter un fichier du tout — il a planté, a été tué, ou n'a produit aucun rapport — `watermarks-remover-clean` nomme ce fichier et sort avec le code 3 à la place, afin qu'un nettoyeur qui a échoué ne soit jamais confondu avec un fichier déjà propre. Exécutez l'un ou l'autre manuellement avec `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
Journal des modifications
v0.7.0 — réécriture de la couche B dans /clean, module de vol de filigrane, suppression de filigrane audio/vidéo et élargissement des benchmarks/outils
v0.7.0 intègre la réécriture de marque statistique de la couche B directement dans le service /clean, pilotée par une stratégie configurable et optimisée par benchmark ([email protected],[email protected]). En parallèle : un module de vol de filigrane en boîte noire, une suppression destructive de filigrane audio et vidéo image par image, un benchmark de réécriture nettement plus riche, et une série de correctifs de durcissement, de sécurité et d'outillage.
Réécriture de la couche B dans le service
/cleanexécute la réécriture de la couche B pour le texte après la couche A. La valeur par défaut provient deconfig/clean_strategy.json; unoptions.strategypar requête la remplace, et/cleanrejette avec un code 400 lorsque le backend requis n'est pas configuré (#315). Priorité de configuration :--strategy-config>WATERMARKS_CLEAN_STRATEGY_FILE>config/clean_strategy.json.- Nouvelle tactique de réécriture
mlm: masquer une fraction des mots de contenu et compléter avecroberta-large— une édition locale non autorégressive, de sorte que la sortie mélange le flux de tokens original avec les prédictions du LM masqué (#311). - La tactique
humanizeapplique désormais la passe de compétence d'humanisation de manière déterministe (guillemets droits, pas de tirets em/en, effondrement des mots de remplissage,utilize→use) et nomme les règles d'écriture humaine dans le prompt (#311).rewrite_text.pya gagné un chemin CLI--strategy. - Exactitude de la réécriture : tokenisation des mots Unicode dans la divergence lexicale (#305) ; comparaison des marges brutes avant arrondi et enregistrement des métadonnées de sélection / p-values classées (#249).
Benchmark
- Recherche de recette SynthID + mesure robuste (#280) ; renommage du vocabulaire de réécriture, recherche multi-entrées et ordre humanize-en-dernier (#302) ; ne recommander que les stratégies qui passent encore après le polissage d'humanisation (#307).
- API en masse Pangram comme backend de ressemblance humaine (#296) ; benchmark durci au niveau de réécriture minimale avec un corpus de 30 documents (#257) ; grille de poids validée + recherche de recette élargie (#294) ; corpus de benchmark polonais (#295).
Vol de filigrane
- Nouveau module de vol de filigrane en boîte noire et téléchargeur de corpus de prompts (#303) ; effacement de l'état obsolète en cas d'échec de la sonde de redémarrage (#310).
Audio / vidéo / image
- Chaîne destructive de suppression de filigrane audio pour silentcipher/AudioSeal/WavMark (tempo + pitch + EQ + ré-encodage à bas débit → M4A) (#266).
- Purification vidéo TrustMark image par image qui effondre le vote temporel (#265).
- Boîte
uuidde provenance de contenu C2PA reconnue sur MP4/MOV/AVIF/HEIC (#264). - Préservation des queues MP4 tronquées lors du stripping (#242) ; maintien de la destination de ré-encodage audio distincte de la destination de nettoyage du conteneur (#278).
- Ignorance de la sortie exiftool rejetée et du SynthID redondant dans le scan post-nettoyage (#261) ; dégradation propre lorsque exiftool ne peut pas traiter un PDF (#281).
- Plafonnement des
zTXt/iTXtPNG décompressés à 1 MiB (#308) ; suppression des déclarations DOCTYPE/ENTITY XML SVG (#288) ; préservation de la sécurité octet des membres binaires DOCX (#314) ; préservation deAppVersionOOXML (#289).
Service HTTP et CLI
- Option
/cleanpour conserver les espaces exotiques, en miroir de la CLI (#274) ;/inspectexpose des classes de preuves explicites dans la charge utile suspecte (#277) ; horodatages dans les journaux de requêtes HTTP (#256) ; transmission des octets de charge utile dans le scoring SynthID HTTP etinspect_*pour éviter une relecture redondante. clean_file.pya gagné-q/--quiet/--only-changed(#254).
Compétences, plugin et hooks
- Scoring de stylométrie et leviers de détection pour
clean-user-facing-text(#258) ; lanceur de hook PostToolUse rendu multiplateforme (#255) ; le hook pre-commit traite les fichiers non textuels propres et identiques octet par octet comme modifiés (#238).
Audit
audit_dir.pyanalyse les fichiers source, de documentation et i18n que le routeur a ignorés (#284) ; analyse.ts/.tsx/.jsx/.gdet aligne la confiance spatiale entre les formats (#273) ; prise en charge deaudit_website.py --sarif(#194) ; durcissement des sauvegardes en place, du statut des fichiers propres, du verdict SynthID, des ID3v2 tronqués et du routage zip (#201).
Sécurité
- Suppression du ReDoS polynomial dans les scans data-URI et JSON-LD (#306) ; blocage des redirections HTTP dans le scorer SynthID pour prévenir les SSRF (#252).
CI, outillage et documentation
- La CI échoue lorsque les exigences des backends optionnels ne peuvent pas être résolues (#301) ; l'image Docker signale ffmpeg comme utilisable et installe Ghostscript (#272) ; mises à jour de dépendances (cython #299, scipy #298, ruff #297, docker/setup-buildx-action #237).
- Documentation : section Watermark Detectors, référence au blog ETH SRI « Probing SynthID », politique Ecosystem (suppression de ClaudeWatermarks ; exigence que les projets listés utilisent ce dépôt) (#292).
v0.6.0 — couverture de formats élargie, durcissement de la couche A, distribution de plugin et hooks, et réécriture guidée par la détection
Couverture des formats et conteneurs
- AVIF / HEIC : métadonnées stdlib natives et suppression C2PA (#84, #85)
- BMP / GIF / TIFF : détection stdlib, inspection et nettoyage des métadonnées — les extensions de commentaire/XMP GIF sont supprimées tandis que le bouclage
NETSCAPE2.0et les autres chunks d'animation sont préservés ; les métadonnées IFD TIFF (XMP/EXIF/GPS/IPTC/MakerNote) sont supprimées avec les charges utiles mises à zéro et les offsets de strip conservés, pour le TIFF classique et le BigTIFF ; les métadonnées de fin BMP sont tronquées avec réécriture du champ de taille de fichier (#107) - EPUB : nettoyage de conteneur stdlib — métadonnées OPF et meta/JSON-LD XHTML nettoyés, médias raster/SVG intégrés supprimés, couche A appliquée au texte du corps XHTML, parties de métadonnées porteuses de marqueurs supprimées, et parties chiffrées OCF transmises sans modification (#107)
- XLSX / PPTX / DOCX (OOXML) : nettoyage natif stdlib des métadonnées de conteneur, du texte et des médias intégrés ; champs de provenance
docPropsDOCX toujours vidés ; élagage des relations pendantes après suppression decustomXml; exécution de la couche A sur le texte du corps DOCX/ODT ; décodage des entités XML avant le nettoyage de la couche A (#91, #100, #76, #83, #73, #80, #74, #81, #142) - Conteneurs SGML/vectoriels : suppression de métadonnées en temps linéaire pour SVG/ODT (GHSA-7vpp-96qp-j9wh) (#147) ; inspection et nettoyage récursifs des data URIs raster intégrés dans les SVG, HTML et Markdown (#87, #88)
- Audio / vidéo : suppression des métadonnées AI/C2PA pour MP4/MOV, WAV et MP3 (#139) ; détection et suppression du chunk C2PA RIFF WAV ; prise en charge des métadonnées C2PA FLAC ; rejet de l'analyse partielle des trames ID3v2 (#232) ; préservation des offsets média MP4 lors du stripping des métadonnées (#183)
- PDF : atteindre les métadonnées qui résident dans les images intégrées et arrêter le redimensionnement du PDF pour supprimer le XMP ; exécution de la passe d'image profonde que exiftool soit installé ou non ; respect des octets de remplissage des marqueurs JPEG et partage d'un seul parcoureur de segments
- PNG : détection des noms de produits de générateurs IA dans les métadonnées textuelles PNG ; détection des marqueurs IA dans le texte PNG compressé (#127) ; conservation de la queue tronquée au lieu de la supprimer dans les strips png/isobmff (#182)
Durcissement de la couche A (Unicode invisible)
- Durcissement consolidé de la couche A (#133) : suppression des points de code
Default_Ignorableréservés sans usage d'échange légitime (U+2065,U+FFF0–U+FFF8,U+E0000,U+E0080–U+E00FF,U+E01F0–U+E0FFF— signalés commereserved_ignorable), des 66 non-caractères (U+FDD0–U+FDEFplusU+FFFE/U+FFFFpar plan — signalés commenoncharacter), et de trois porteurs Default_Ignorable à rendu vide que le fourre-toutCfn'a jamais vus (U+180F,U+3164,U+FFA0). Chacun bénéficie de la même préservation en contexte que ses frères déjà couverts, de sorte que le texte à syllabes partielles n'est pas corrompu, et chacun est appliqué à la fois au moteur du service et à la copie allégée de la compétence vendored - Arrêt de la suppression des contrôles de format de mise en page visible adjacents à leur propre écriture : les contrôles de quadrat hiéroglyphique égyptien (
U+13430–U+1343F), les contrôles de sténographie Duployan (U+1BCA0–U+1BCA3) et les contrôles musicaux de beam/tie/slur/phrase (U+1D173–U+1D17A) sont désormais préservés lorsqu'ils sont adjacents à leur propre écriture et toujours supprimés (et signalés) lorsqu'ils flottent entre des textes non apparentés ; le mode paranoïaque--strip-emoji-glueles supprime toujours partout - Polissage emoji / écriture : préservation de VS16 après les emojis isolés en dehors des plages de blocs ; préservation des joiners d'écriture, des emojis drapeaux et des marques Cf arabes ; préservation de l'Unicode multilingue lors du nettoyage de texte (#34)
Réécriture de la couche B et détection de filigrane
- Réécriture itérative de la couche B guidée par la détection : chaque tour génère des variantes
--candidates(par défaut 1,WATERMARKS_REWRITE_CANDIDATES) et--max-loops(par défaut 1,WATERMARKS_REWRITE_LOOPS) plafonne les tours d'évaluation, s'arrêtant dès qu'une tentative passe la détection. Priorité de l'évaluateur : MarkLLM (--markllm-scheme) > divergence lexicale bigram-Jaccard (repli).rewrite_text.py --json-statsrapporte désormaisevaluator/max_loops/attempts_made/passedet lescandidate_scorespar tentative (#153) - Vérification à clé identique Keyed-Gumbel (Aaronson EXP) : le nouveau
detect_gumbel.pyuniquement stdlib implémente le test de rejeu sans modèle (u = PRF(Hash(key, window), token) ; p-value exacte de queue Gamma ; masquage de fenêtre répétée) sans GPU, modèle ni logits.rewrite_text.py --gumbel-key(envWATERMARKS_GUMBEL_KEY, préféré) en fait l'évaluateur de la boucle itérative (priorité : gumbel > markllm > divergence lexicale) et il est exposé commegumbeldans/capabilitieset/detect. Clé identique uniquement — pas un oracle de fournisseur ; la clé n'est jamais journalisée (#190) - Benchmarks : benchmark texte MarkLLM multi-schémas et détection (#188) et un benchmark reproductible de suppression de texte SynthID (#145) ; variantes par défaut
paraphrase:3; le rapport et le CSV portent les tentatives par document (colonnesmean_attempts/att,attempts/evaluator/passed) ;--rewrite-loopsreflète--max-loops - Détection : détection de filigrane textuel de fournisseur (Gemini SynthID, Claude seam, MarkLLM) plus un sidecar de scoring d'image SynthID (#109) ; nouveau détecteur de texte IA statistique et stylométrique sans LLM pour la CI et les audits (#68, #69)
Distribution : plugin, hooks et installations de compétences
- Le dépôt est désormais un plugin Claude Code et un marketplace à plugin unique (
.claude-plugin/plugin.json+marketplace.json), de sorte que les deux compétences s'installent avec/plugin marketplace add guillaumemeyer/watermarks-removerpuis/plugin install watermarks-remover@watermarks-remover, et se mettent à jour sur place.make plugin-validateexécuteclaude plugin validate . --strict;tests/test_plugin_manifest.pyvérifie les manifestes sans la CLI install_skill.pya gagné un--target(claude-code,claude-project,cowork,cursor) et un sélecteur--skillcouvrant les deux compétences livrées, plus--list,--linketCLAUDE_CONFIG_DIR. La ciblecoworkconstruit un bundle de téléversement reproductible (dist/<skill>.zip, répertoire de compétence unique au niveau supérieur) ; chaque cible est validée selon les règles d'empaquetage Agent Skills et la limite de téléversement de 30 Mo. Nouvelles ciblesmake:install-claude-code-skill,install-claude-code-text-skill,install-claude-project-skill,package-cowork-skill,package-cowork-text-skill- Nettoyage automatique déterministe via un hook
PostToolUse(hooks/hooks.json+service/scripts/hook_written_file.py) : après que l'agent écrit un fichier, le harnais exécute le hook que le modèle coopère ou non.check(par défaut) signale les marques au modèle ;cleanles supprime sur place et indique au modèle que le fichier a été déplacé, en ne permutant qu'en cas de différence réelle afin que les fichiers propres conservent leur mtime. Le mode provient du paramètrehook_modedu plugin ou deWATERMARKS_HOOK_MODE; la détection réutiliseaudit_lib.scan_file/is_actionable, de sorte que le hook, la barrière pre-commit et l'export SARIF de la CI concordent. Un hook ne peut toujours pas réécrire le message de chat de l'assistant — un tel point d'accroche n'existe pas — donc ce chemin reste au mieux - Intégration du hook pre-commit pour la vérification/nettoyage des fichiers indexés (#138) ; compétence texte Cursor allégée (#35) ; la description de
clean-user-facing-textne nomme plus Cursor comme seul hôte
Service HTTP
- Points de terminaison par lots :
POST /clean/batch,/inspect/batch(#137) etPOST /detect/batch(#151) - Préservation des extensions de format d'image dans
/cleanet utilisation d'écritures sûres dansav_meta(#150) ; utilisation de base64 portable dans l'exemple curl de/detect(et correction de la portabilitérealpathmacOS dans les bootstraps, #185)
Audit / inspection et sécurité
audit_dir.pya gagné la concurrence multi-workers et l'export SARIF 2.1.0 (#101, #102)- Routage des formats binaires du site web vers leurs vrais scanners (#177) ; refus des bombes DTD/entité dans l'analyseur de sitemap (GHSA-pjg6-92pm-mmcf) (#146) ; un nettoyeur planté bloque le commit au lieu d'être lu comme propre (#179) ; un fichier texte illisible est un scan échoué, pas un scan propre (#169)
Correctifs de fiabilité et d'exactitude
- Une seconde exécution
--in-placepréserve le.bakoriginal ; conservation des preuves collectées lorsqu'un membre zip ultérieur échoue à la lecture (#175) ; les conteneurs ISOBMFF tronqués exécutent toujours le repli de scan d'octets C2PA (#176) ; distinction entre un nettoyeur échoué et un fichier déjà propre (#159, #161) ; traitement d'une exécution c2patool échouée comme non concluante plutôt que « pas de C2PA » (#156) ; validation des types d'options de nettoyage (#111) ; ne jamais sélectionner automatiquement le périphérique MPS pour la détection de filigrane textuel (#99) ; portabilité macOS — sortie stdout pure--jsonpour le scorer SynthID et sonderealpathBSD (#70) ; correction d'un chemin Windowssubprocess_creationflagsdans_ghostscript_usableet arrêt de l'ouverture d'une fenêtre de console par les processus enfants sous Windows - Durcissement comportemental : préservation du mode keep des commentaires JPEG bénins ; correction du drapeau avalé de
bench-synthid-text; simplification du passage des drapeaux pour la sonde Ghostscript et suppression du noqa inutile de clean_text (lint)
CI / outillage / documentation
- Linting et formatage Ruff avec application en CI (#103) ; ajout de macOS à la matrice de tests (#152) ; ajout d'une configuration CodeRabbit pour les revues de PR automatisées (#222) ; CODEOWNERS pour CODE_OF_CONDUCT/LICENSE et les propriétaires de revue principale ; attribution du copyright à Guillaume Meyer et aux contributeurs (#228)
- Documentation : conseils de réécriture préservant la voix et protection des choix de voix/accessibilité ; ajouts à l'Ecosystem (ClaudeWatermarks, unmark-web) et une note décourageant les noms similaires ; référence arXiv 2402.14904 ; guide de démarrage automatique Windows via le Planificateur de tâches ; base64 portable dans les exemples curl ; épinglage du moteur texte de la compétence Cursor vendored sur la copie du service (#96)
Non publié
- Hook de nettoyage pre-commit (
watermarks-remover-clean/clean_staged.py) : utilisation de digests de contenu (SHA-256) et détection d'action active afin que les fichiers propres sur disque soient reconnus sans exiger un re-staging infini (#173) - Préservation du conteneur OOXML : maintien de
<AppVersion>intact dansdocProps/app.xmllors du nettoyage des métadonnées DOCX, XLSX et PPTX pour satisfaire les contraintes de schéma ECMA-376 et éviter les erreurs « contenu illisible » de Microsoft Word/Office (#283)
v0.5.0 — distribution service et Docker, API HTTP et harnais de vérification
Distribution service / Docker
- Séparation compétence/service : la compétence (
skills/remove-ai-marks/) est désormais un client distant sans code via HTTP ; toute l'implémentation a été déplacée versservice/scripts/et s'exécute derrièreserver.py, un point d'entrée HTTP stdlib (/health,/inspect,/clean,/capabilities) - Service HTTP :
service/scripts/server.pyexpose le pipeline de nettoyage via JSON/base64 ; le durcissement reflète les CLI (plafonds de taille, garde binaire, écritures atomiques, boucle locale par défaut, authentification bearer optionnelleWATERMARKS_SERVER_API_KEY) - OpenAPI :
GET /openapi.jsonsert une spécification OpenAPI 3.0.3 générée dynamiquement (construite à partir de la table de routage + configuration en direct, de sorte qu'elle ne dérive jamais des points de terminaison réels) ; la CI la valide avecopenapi-spec-validator - Image Docker de base (
service/Dockerfile) : service de nettoyage complet avec exiftool / qpdf / c2patool préinstallés ; toute CLI reste exécutable en surchargeant la commande - Docker / compose :
compose.yamlmonte toute l'infrastructure (coretoujours ;markllm/markdiffusionderrièreprofile: harness;ctrlregen/synthidderrièreprofile: heavyen tant que builds locaux uniquement) ; les services sont préfixéswr-; les services harness/heavy ont par défautcommand: ["--help"]afin quedocker compose up --profile harness --profile heavyse termine proprement (les CLI one-shot sont exécutées avecdocker compose run) ; nouveauxmake compose-check/compose-check.shvalident la pile en cours d'exécution (code de sortie uniquement) - Publication GHCR :
.github/workflows/release-images.ymlpublie les imagescore,markllm,markdiffusionsur les tagsv*;ctrlregen/synthidne sont jamais publiés (licence amont) - Configuration par variables d'environnement :
.env.example+ guide de configuration du service ;docker composecharge automatiquement.env;.envest gitignoré (refus par défaut) - Hygiène du dépôt :
.gitignoreetservice/.dockerignoresont désormais en refus par défaut — seuls les chemins explicitement autorisés peuvent être commités ou envoyés dans un contexte de build (les contextes d'image ne livrent queservice/scripts/, ce qui correspond à tout ce que les Dockerfiles COPY) - Tests :
tests/test_http_server.py(13 cas) pour le service HTTP ; toutes les suites sont redirigées versservice/scripts/
Harnais de filigrane d'image MarkDiffusion (optionnel)
- Nouveau harnais optionnel (externe
THU-BPM/MarkDiffusion, Apache-2.0) :markdiffusion_harness.pyavec les sous-commandeswatermark/detect/purifypour neuf schémas d'image (Tree-Ring, Ring-ID, ROBIN, WIND, SFW, Gaussian-Shading, GaussMarker, PRC, SEAL) clean_image.py --remove-pixel diffusionexécute l'attaque de régénérationDiffusionPurificationde MarkDiffusion comme moteur alternatif de suppression de pixels (intensité conservatrice 0.3 par défaut)- Bootstrap
setup_markdiffusion.sh(épinglage PyPI1.0.2; clone éditable--checkoutà un commit épinglé) +requirements-markdiffusion.txt+Dockerfile.markdiffusionet Makefilebootstrap-markdiffusion/smoke-markdiffusion/docker-markdiffusion-build/docker-markdiffusion-help - Tests basés sur des mocks (
tests/test_markdiffusion_harness.py) — pas de torch en CI ; document de référencereferences/markdiffusion.md - Documentation : mise en garde sur la vérification à schéma identique uniquement (pas un oracle de détecteur de fournisseur) et mise en garde sur la dérive de régénération aveugle dans le README, SKILL.md,
removal-matrix.md,markdiffusion.md
Harnais de filigrane textuel MarkLLM (optionnel)
- Nouveau harnais optionnel (checkout externe
THU-BPM/MarkLLM, Apache-2.0) :detect_text_watermark.pyavec les sous-commandesdetect/watermarkpour les schémas KGW et SynthID rewrite_text.py --markllm-schemeexécute la détection avant/après autour d'une réécriture de couche B et la détection par candidat lorsque--candidates N>1(contrôlé par variable d'environnement ; rapportecleared)- Bootstrap
setup_markllm.sh+requirements-markllm.txt(dépendances épinglées) +Dockerfile.markllmet Makefilebootstrap-markllm/smoke-markllm/docker-markllm-build/docker-markllm-help - Durcissement : chargement de modèle en cache uniquement
--offline(pas de sortie HF, pas de code distant), plafond de configuration de 1 MiB,WATERMARKS_MARKLLM_RLIMIT_ASoptionnel sur le sous-processus de réécriture, torch épinglé dans le Dockerfile, et vérification du SHA de clone dansDockerfile.markllm - Tests basés sur des mocks (
tests/test_markllm_detect.py, 21 cas) — pas de torch en CI ; mise en garde du harnais de vérification (configuration identique uniquement, pas un oracle de détecteur de fournisseur) documentée dans le README, SKILL.md,removal-matrix.md,vendor-notes.md
Correctifs et polissage- Layer B : rewrite_text.py envoie désormais reasoning_effort: "none" par défaut pour les backends openai-compatible (--reasoning-effort / WATERMARKS_REWRITE_REASONING_EFFORT ; off l'omet). Les modèles de raisonnement comme deepseek-v4-flash brûlent sinon ~100s de chaîne de pensée sur une réécriture d'une ligne (9 894 vs 12 tokens de complétion)
- Correction du build d'image markllm :
requirements-markllm.txtépinglaittokenizers==0.23.1, ce qui entre en conflit avectransformers==5.15.0(plafonnetokenizers<=0.23.0; aucune version 0.23.0 n'existe) — désormais épinglé àtokenizers==0.22.2; torch déplacé vers l'index de wheels CPU (torch==2.13.0.*) afin que l'image soit CPU-only commeDockerfile.markdiffusion - Correction du build d'image ctrlregen : les épinglages de recherche de l'époque 2023 (
safetensors==0.4.3,transformers==4.37.2→tokenizers<0.19) ne fournissent pas de wheels Python 3.14, l'image de base est donc désormaispython:3.11-slim(épinglée par digest, multi-arch) - Correction des images de harnais à l'exécution :
Dockerfile.markllmetDockerfile.markdiffusionne copiaient jamaiscommon.pydans/app(bug préexistant) — ajouté - WebP : inspection et nettoyage des métadonnées en stdlib uniquement pour les chunks RIFF
C2PA, XMP, EXIF et profil ICC (#37) - BMP / GIF / TIFF : détection, inspection et nettoyage des métadonnées en stdlib uniquement — les extensions commentaire/XMP des GIF sont supprimées tandis que le bouclage
NETSCAPE2.0est préservé ; les métadonnées IFD TIFF (XMP/EXIF/GPS/IPTC/MakerNote) sont supprimées avec les payloads mis à zéro et les offsets de strip conservés, pour le TIFF classique et le BigTIFF ; les métadonnées en fin de BMP sont tronquées avec réécriture du champ de taille de fichier - EPUB : nettoyage de conteneur en stdlib uniquement — métadonnées OPF et meta/JSON-LD XHTML nettoyées, médias raster/SVG embarqués supprimés, Layer A appliqué au texte du corps XHTML, parties de métadonnées porteuses de marqueurs supprimées, et parties chiffrées OCF transmises sans modification
- Assainissement des noms de fichiers : le service HTTP refuse les noms de sortie non sûrs fournis par le client
- Correction du nettoyeur de frontmatter markdown qui plantait sur les clés IA imbriquées et les laissait fuiter (#25)
- Les outils texte refusent les entrées binaires ;
--force-textforce le passage (#24) --jsonne supprime plus le code de sortie de signal résiduel (#30)inspect_fileaffiche le nom du fichier dans sa sortie (#50)- Préserver les balises meta generator CMS à casse mixte (#42)
- Préserver les invisibles porteurs de scripts, supprimer les PUA dans Layer A (#38, #52)
- Préserver les joiners de scripts, les emoji drapeaux et les marques Cf arabes dans Layer A (#28)
- Durcir l'audit de site web contre les SSRF et les bombes gzip (#49)
- SECURITY.md ne référence que le canal d'avis privés (#51)
- Windows : ports PowerShell des bootstraps d'installation (#40)
- Docs : ajout des badges stars/forks et suppression du graphique star-history ; ajout de MarkLLM aux références du README ; modèle de pull request ; plan pour le déploiement Docker CLI + API
v0.4.0 — suppression de pixels, confiance des détections, corrections Windows et faux positifs
Suppression optionnelle de pixels CtrlRegen (backend externe)
- Suppression optionnelle de watermark dans le domaine des pixels via un checkout externe
mertizci/noai-watermark: adaptateurclean_ctrlregen.py+ bootstrapsetup_ctrlregen.sh(commit épinglé, sparse checkout, venv, vérification SHA), plusDockerfile.ctrlregenetmake bootstrap-ctrlregen/docker-ctrlregen-build/smoke-ctrlregen clean_image.py --remove-pixel ctrlregenexécute strip des métadonnées → suppression CtrlRegen → score avant/après reverse-SynthID optionnel ;inspect_image.pysuggère le flag en cas de score SynthID élevé- Intensité par défaut conservatrice
0.25(presets 0.15/0.25/0.35/0.5/0.7) ; le pipeline natif 512×512 est auto-tuilé par le backend pour les images plus grandes ; le sous-processus torch obtient des plafonds de ressources plus élevés, surchargeables par variables d'environnement - Le backend n'est jamais embarqué :
noai-watermarkne fournit aucun fichier LICENSE (traité comme tous droits réservés), et ses chemins d'auto-installation/redémarrage sont contournés en utilisant directementCtrlRegenEngine
Confiance des détections et audits agrégés
- Les détections sont désormais classées
confirmed/probable/informational/likely_false_positive, exposées dans les rapports JSON texte/image/conteneur et humains - Nouveaux
audit_dir.py(arborescence récursive) etaudit_website.py(découverte de sitemap + crawl) qui agrègent les rapports ; documentés dans SKILL.md
Corrections de faux positifs
- DOCX : scanner uniquement
docProps/customXml, pas le corps visible (#14) - Text Layer A : préserver les emoji
VS16/ZWJaprès une base emoji ; nouveau flag paranoïaque--strip-emoji-glue(#22) - HTML : traiter les balises generator CMS comme informatives, pas comme métadonnées IA (#13)
- PDF : exclure les payloads de flux du scan d'octets de marqueurs IA (#13)
- Les rapports d'inspection signalent les chemins non pris en charge/best-effort
Support Windows
- Encadrer
preexec_fnetos.fchmod, réservés à POSIX, afin que les écritures et les outils optionnels fonctionnent sous Windows (#15, #23) - Reconfigurer stdio en UTF-8 afin que les flux Windows redirigés ne lèvent plus d'erreur sur l'Unicode invisible ; job CI Windows + exécution smoke CLI (#23)
Docs et chaîne d'approvisionnement
- Section CtrlRegen du README + références de recherche (CtrlRegen, UnMarker, mise en garde forensic-stealth), clause de non-responsabilité d'usage responsable ; mises à jour SKILL/matrix/vendor-notes/ethics
- Configuration Dependabot + CODEOWNERS des chemins de sécurité ; mise à jour de scipy/numpy/opencv-python/scikit-learn/pywavelets et de l'image de base vers Python 3.14-slim
- Tests CtrlRegen basés sur des mocks (pas de torch en CI)
v0.3.2 — durcissement de la sécurité (écritures sûres, client HTTP, chaîne d'approvisionnement CI)
- Écritures de sortie sûres et atomiques : chaque nettoyeur écrit désormais via fichier temporaire + renommage atomique (
safe_write_bytes/safe_write_text), refuse les destinations sous forme de liens symboliques, et crée des sauvegardes.bakvia le même chemin sûr — les liens symboliques préplacés (par ex. dans/tmpou les répertoires de téléchargement) ne peuvent plus rediriger une écriture propre vers un fichier arbitraire - Durcissement du client HTTP de
rewrite_text.py: les redirections sont refusées d'emblée, afin qu'une clé API dans l'en-têteAuthorizationne puisse jamais être renvoyée vers un hôte non validé ; les endpoints non-loopback sont refusés par défaut (opt-in avec--allow-remoteouWATERMARKS_REWRITE_ALLOW_REMOTE=1) ; seuls les schémas http(s) sont acceptés ;--api-keya été supprimé — les clés passent uniquement par l'environnement viaWATERMARKS_REWRITE_API_KEY - Plafonds de ressources : entrée max par défaut 1 GiB → 256 MiB, nouveau plafond stdin de 64 MiB, budget zip DOCX/ODT 512 MiB → 128 MiB, et
RLIMIT_AS/RLIMIT_FSIZEappliqués aux sous-processus exiftool/c2patool/SynthID (tous les plafonds surchargeables par variables d'environnement) - Chaîne d'approvisionnement : actions CI épinglées par SHA avec
permissions: contents: read, dépendances de dev épinglées (requirements-dev.txt), une étapepip-audit, et un nouveau workflow CodeQL ; l'image Docker s'exécute désormais en tant qu'utilisateur non privilégié avec pip épinglé - Dépendances du scorer : Pillow mis à jour de 10.4.0 → 12.3.0 (24 CVE connues) ; usage de l'API vérifié par rapport au commit upstream épinglé
- Tests : 18 nouveaux tests de régression de sécurité (60 au total, tous passants)
v0.3.1 — réécriture Layer B plus forte des watermarks statistiques
- La paraphrase par défaut de
rewrite_text.pyeffectue désormais une attaque explicite choix de mots + syntaxe (ordre des propositions, connecteurs, mots de transition, frontières de phrases, mots fonctionnels) plutôt qu'une réécriture générique - Nouvelle
--tactic humanize: passe zero-shot « écrire comme un humain » ciblant les formulations IA stéréotypées - Nouvelle
--tactic code: réécrit les commentaires, docstrings et littéraux de chaînes, et renomme les identifiants locaux tout en préservant le comportement et les noms d'API publics - La passe structurelle émet désormais une « prose humaine naturelle et variée » au lieu du « style professionnel clair » typique de l'IA
- Nouvelle
--temperature(par défaut0.9) pour les backends Ollama et OpenAI-compatible - Nouvelle
--candidates N: génère N réécritures et sélectionne la plus divergente lexicalement (distance de Jaccard sur les bigrammes) avec une garde contre la dérive de longueur - Hygiène de modèle renforcée : préférer les modèles locaux à poids ouverts et éviter tout fournisseur connu comme watermarkeur, pas seulement l'origine suspectée
- Le rapport de risque résiduel distingue désormais les textes courts/hautement prévisibles (risque plus faible) de la prose longue à haute entropie (risque plus élevé)
- Docs mises à jour dans
SKILL.md,removal-matrix.mdetvendor-notes.md; les tests couvrent les nouveaux prompts, le scoring de divergence et la sélection de candidats
v0.3.0 — scoring optionnel des pixels SynthID
- Scorer SynthID optionnel dans le domaine des pixels via un checkout externe
aloshdenny/reverse-SynthID(score_synthid.py) ; exposé dansinspect_image.py/clean_image.pyavecREVERSE_SYNTHID_DIRou--synthid-dir - Bootstrap
setup_synthid.sh(dépendances du scorer uniquement ;--fullinstalle les requirements upstream) ;Dockerfile.synthidplusmake docker-synthid-build/docker-synthid-help - Cibles Makefile
smoke-synthidetbootstrap-synthid - Tests pour l'adaptateur du scorer, le chemin CLI indisponible, le parsing JSON et les erreurs d'exécution
- Docs : détection/scoring uniquement (pas de suppression de pixels) ; le code upstream n'est pas embarqué et reste sous sa licence de recherche non commerciale
v0.2.0 — correction de faux positif c2patool
image_meta.py:has_manifestne signale plusError: No claim found/No JUMBF data foundcomme un manifeste (bug de priorité d'opérateurs : les marqueurs négatifs annulent désormais toutes les branches positives)- Nouveau
tests/test_c2patool_report.py(4 cas : pas de claim, pas de JUMBF, manifeste authentique, outil absent) - Docs : liens
c2patoolcorrigés (dépôt déplacé verscontentauth/c2pa-rs) ; ajout d'une mise en garde sur le coût en qualité de la suppression de watermark textuel
v0.1.0 — finitions de packaging + honnêteté sur la provenance
Makefile(test/smoke/install-skill) etpytest.ini- Échantillons de fixtures pour Markdown, HTML, SVG ; test de nettoyage dégradé PDF
- Docs : modèle industriel à deux couches (C2PA à liaison forte vs liaison douce / SynthID-media)
- Tableau de risque résiduel du README + liens vers des outils de vérification externes
- Référence : guide C2PA/SynthID de l'Institute of AI PM
- Watermarks à liaison douce et pixel/audio/vidéo explicitement hors périmètre dans skill/matrix/ethics
v0.0.1 — version initiale multi-fournisseurs
- Skill d'agent
remove-ai-marks(remplaceremove-claude-marks, réservé à Claude) - Layer A : Unicode invisible / bidi / caractères de balise / homoglyphes d'espace (
inspect_text/clean_text) - Layer B : conseils de réécriture +
rewrite_text.pyoptionnel (print-prompt, Ollama, OpenAI-compatible) - Fichiers : strip des métadonnées C2PA/IA pour PNG, JPEG, SVG, PDF, DOCX, ODT, HTML, Markdown
inspect_file.py/clean_file.pyunifiés- Docs multi-fournisseurs (Claude, Gemini/SynthID-class, OpenAI, open-LLM)
- Scripts stdlib-first ;
c2patool/exiftooloptionnels
License
MIT — voir LICENSE.
Bibliography
- How Claude marks AI-generated content (Anthropic)
- Dathathri et al., Scalable watermarking for identifying large language model outputs (SynthID-Text, Nature 2024)
- Google AI for Developers, SynthID safeguards (Gemini API docs)
- C2PA / c2patool
- Kirchenbauer et al., A Watermark for Large Language Models
- Evseev, D. (Arbitration City), Accurate, Costless, and Invisible AI Text Watermarking for Self-Hosted AI Inference (rapport technique, août 2026) — watermarking keyed-Gumbel livré dans le moteur open-source arbi-serve, avec détection par test exact et support du speculative decoding — PDF
- THU-BPM/MarkLLM (boîte à outils unifiée pour évaluer les algorithmes de watermarking des LLM)
- Pan et al., MarkDiffusion: An Open-Source Toolkit for Generative Watermarking of Latent Diffusion Models (JMLR) — la boîte à outils d'embedding que le harnais optionnel de watermarking d'images de ce dépôt encapsule — code, docs
- Zhang et al., Watermarks in the Sand: Impossibility of Strong Watermarking for Generative Models (ICML 2024)
- Sander et al., Watermarking Makes Language Models Radioactive — les watermarks survivent au fine-tuning et marquent les modèles en aval entraînés sur des données watermarkées
- Pan et al., Can LLM Watermarks Robustly Prevent Unauthorized Knowledge Distillation? — provenance basée sur le watermark et protection contre la distillation de connaissances
- google-deepmind/synthid-text (référence de recherche ; non utilisée pour la détection ici)
- aloshdenny/reverse-SynthID (référence de recherche)
- ETH Zurich SRI, Probing SynthID (blog de recherche sur la détectabilité des watermarks SynthID)
- Liu et al., Image Watermarks are Removable Using Controllable Regeneration from Clean Noise (ICLR 2025) — la méthode de régénération de pixels que le backend CtrlRegen optionnel implémente — code
- Kassis & Hengartner, UnMarker: A Universal Attack on Defensive Image Watermarking (arXiv:2405.08363 ; IEEE S&P 2025) — une attaque universelle de watermark comparée sur une métrique différente de CtrlRegen
- Goonatilake & Ateniese, Removing the Watermark Is Not Enough: Forensic Stealth in Generative-AI Watermark Removal (arXiv:2605.09203) — motive le défaut d'intensité conservatrice : la suppression peut encore laisser des traces forensiques
- mertizci/noai-watermark (boîte à outils CLI/Python pour la suppression SynthID/StableSignature/TreeRing et le strip des métadonnées IA)
- 0xROOTPLS/DeSynth (suppression SynthID pour les images OpenAI/Google)
- Institute of AI PM, AI Content Provenance and Watermarking: The PM's Guide to C2PA and SynthID (modèle industriel à deux couches : C2PA + watermark imperceptible / liaison douce ; contexte SB 942 / EU AI Act Art. 50)