
Une application axée sur la confidentialité qui supprime les filigranes IA du contenu que vous possédez.
_ _ _ ____ ___ ____ ____ _ _ ____ ____ _ _ ____ ____ ____ _ _ ____ _ _ ____ ____
| | | |__| | |___ |__/ |\/| |__| |__/ |_/ [__ __ |__/ |___ |\/| | | | | |___ |__/
|_|_| | | | |___ | \ | | | | | \ | \_ ___] | \ |___ | | |__| \/ |___ | \
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.
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é)
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.
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/.
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
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
mkdir -p .grok/skills ln -sfn "$(pwd)/skills/remove-ai-marks" .grok/skills/remove-ai-marks
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
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) :
Les scripts core nécessitent uniquement la bibliothèque standard Python 3.10+. Les appels de modèle Layer B sont optionnels.
SCRIPTS=service/scripts
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
python3 "$SCRIPTS/inspect_text.py" draft.md python3 "$SCRIPTS/clean_text.py" draft.md -o draft.cleaned.md --stats
python3 "$SCRIPTS/rewrite_text.py" draft.md --backend print-prompt --tactic paraphrase
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.
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).
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 :
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.
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".
make docker-synthid-build
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.
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.
SCRIPTS=service/scripts
"$SCRIPTS/setup_ctrlregen.sh"
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).
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 :
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.
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.
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 :
--offline sur 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é (le trust_remote_code de
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.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.
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 :
DiffusionPurification est exposée comme clean_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.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
"$SCRIPTS/setup_markdiffusion.sh"
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
MARKDIFFUSION_DIR=~/markdiffusion
~/markdiffusion/.venv/bin/python "$SCRIPTS/markdiffusion_harness.py" purify
wm.png -o wm.purified.png --purification-intensity 0.3 --json
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.
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
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 :
pdfwrite avec 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. never est l'option pour un document dont les flux doivent rester intacts.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.
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) :
Contexte industriel à deux couches (C2PA + filigrane imperceptible) : guide de l'Institute of AI PM.
Vérificateurs fournis par les fournisseurs pour vérifier si un contenu porte des marques de provenance IA :
Matrice : skills/remove-ai-marks/references/removal-matrix.md.
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.
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 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 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 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.
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.
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
repos:
`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
/clean, module de vol de filigrane, suppression de filigrane audio/vidéo et élargissement des benchmarks/outilsv0.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
/clean exécute la réécriture de la couche B pour le texte après la couche A. La valeur par défaut provient de config/clean_strategy.json ; un options.strategy par requête la remplace, et /clean rejette 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.mlm : masquer une fraction des mots de contenu et compléter avec roberta-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).humanize applique 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). a gagné un chemin CLI .Benchmark
Vol de filigrane
Audio / vidéo / image
uuid de provenance de contenu C2PA reconnue sur MP4/MOV/AVIF/HEIC (#264).zTXt/iTXt PNG 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 de AppVersion OOXML (#289).Service HTTP et CLI
/clean pour conserver les espaces exotiques, en miroir de la CLI (#274) ; /inspect expose 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 et inspect_* pour éviter une relecture redondante.clean_file.py a gagné -q/--quiet/--only-changed (#254).Compétences, plugin et hooks
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.py analyse les fichiers source, de documentation et i18n que le routeur a ignorés (#284) ; analyse .ts/.tsx/.jsx/.gd et aligne la confiance spatiale entre les formats (#273) ; prise en charge de audit_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é
CI, outillage et documentation
Couverture des formats et conteneurs
NETSCAPE2.0 et 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)docProps DOCX toujours vidés ; élagage des relations pendantes après suppression de customXml ; 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)Durcissement de la couche A (Unicode invisible)
Default_Ignorable ré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 comme reserved_ignorable), des 66 non-caractères (U+FDD0–U+FDEF plus U+FFFE/U+FFFF par plan — signalés comme noncharacter), et de trois porteurs Default_Ignorable à rendu vide que le fourre-tout n'a jamais vus (, , ). 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 vendoredRéécriture de la couche B et détection de filigrane
--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-stats rapporte désormais evaluator / max_loops / attempts_made / passed et les candidate_scores par tentative (#153)detect_gumbel.py uniquement 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. (env , préféré) en fait l'évaluateur de la boucle itérative (priorité : gumbel > markllm > divergence lexicale) et il est exposé comme dans et . Clé identique uniquement — pas un oracle de fournisseur ; la clé n'est jamais journalisée (#190)Distribution : plugin, hooks et installations de compétences
.claude-plugin/plugin.json + marketplace.json), de sorte que les deux compétences s'installent avec /plugin marketplace add guillaumemeyer/watermarks-remover puis /plugin install watermarks-remover@watermarks-remover, et se mettent à jour sur place. make plugin-validate exécute claude plugin validate . --strict ; tests/test_plugin_manifest.py vérifie les manifestes sans la CLIinstall_skill.py a gagné un --target (claude-code, claude-project, cowork, cursor) et un sélecteur couvrant les deux compétences livrées, plus , et . La cible construit un bundle de téléversement reproductible (, 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 cibles : , , , , Service HTTP
POST /clean/batch, /inspect/batch (#137) et POST /detect/batch (#151)/clean et utilisation d'écritures sûres dans av_meta (#150) ; utilisation de base64 portable dans l'exemple curl de /detect (et correction de la portabilité realpath macOS dans les bootstraps, #185)Audit / inspection et sécurité
audit_dir.py a gagné la concurrence multi-workers et l'export SARIF 2.1.0 (#101, #102)Correctifs de fiabilité et d'exactitude
--in-place préserve le .bak original ; 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 --json pour le scorer SynthID et sonde realpath BSD (#70) ; correction d'un chemin Windows subprocess_creationflags dans _ghostscript_usable et arrêt de l'ouverture d'une fenêtre de console par les processus enfants sous Windowsbench-synthid-text ; simplification du passage des drapeaux pour la sonde Ghostscript et suppression du noqa inutile de clean_text (lint)CI / outillage / documentation
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)<AppVersion> intact dans docProps/app.xml lors 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)Distribution service / Docker
skills/remove-ai-marks/) est désormais un client distant sans code via HTTP ; toute l'implémentation a été déplacée vers service/scripts/ et s'exécute derrière server.py, un point d'entrée HTTP stdlib (/health, /inspect, /clean, /capabilities)service/scripts/server.py expose 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 optionnelle WATERMARKS_SERVER_API_KEY)GET /openapi.json sert 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 avec openapi-spec-validatorHarnais de filigrane d'image MarkDiffusion (optionnel)
THU-BPM/MarkDiffusion, Apache-2.0) : markdiffusion_harness.py avec les sous-commandes watermark / detect / purify pour neuf schémas d'image (Tree-Ring, Ring-ID, ROBIN, WIND, SFW, Gaussian-Shading, GaussMarker, PRC, SEAL)clean_image.py --remove-pixel diffusion exécute l'attaque de régénération DiffusionPurification de MarkDiffusion comme moteur alternatif de suppression de pixels (intensité conservatrice 0.3 par défaut)setup_markdiffusion.sh (épinglage PyPI 1.0.2 ; clone éditable --checkout à un commit épinglé) + requirements-markdiffusion.txt + Dockerfile.markdiffusion et Makefile bootstrap-markdiffusion / / / Harnais de filigrane textuel MarkLLM (optionnel)
THU-BPM/MarkLLM, Apache-2.0) : detect_text_watermark.py avec les sous-commandes detect / watermark pour les schémas KGW et SynthIDrewrite_text.py --markllm-scheme exé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 ; rapporte cleared)setup_markllm.sh + requirements-markllm.txt (dépendances épinglées) + Dockerfile.markllm et Makefile bootstrap-markllm / smoke-markllm / docker-markllm-build / 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)
requirements-markllm.txt épinglait tokenizers==0.23.1, ce qui entre en conflit avec transformers==5.15.0 (plafonne tokenizers<=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 comme Dockerfile.markdiffusionsafetensors==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ésormais python:3.11-slim (épinglée par digest, multi-arch)Dockerfile.markllm et ne copiaient jamais dans (bug préexistant) — ajoutéSuppression optionnelle de pixels CtrlRegen (backend externe)
mertizci/noai-watermark : adaptateur clean_ctrlregen.py + bootstrap setup_ctrlregen.sh (commit épinglé, sparse checkout, venv, vérification SHA), plus Dockerfile.ctrlregen et make bootstrap-ctrlregen / docker-ctrlregen-build / smoke-ctrlregenclean_image.py --remove-pixel ctrlregen exécute strip des métadonnées → suppression CtrlRegen → score avant/après reverse-SynthID optionnel ; inspect_image.py suggère le flag en cas de score SynthID élevé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'environnementnoai-watermark ne fournit aucun fichier LICENSE (traité comme tous droits réservés), et ses chemins d'auto-installation/redémarrage sont contournés en utilisant directement Confiance des détections et audits agrégés
confirmed / probable / informational / likely_false_positive, exposées dans les rapports JSON texte/image/conteneur et humainsaudit_dir.py (arborescence récursive) et audit_website.py (découverte de sitemap + crawl) qui agrègent les rapports ; documentés dans SKILL.mdCorrections de faux positifs
docProps/customXml, pas le corps visible (#14)VS16/ZWJ après une base emoji ; nouveau flag paranoïaque --strip-emoji-glue (#22)Support Windows
preexec_fn et os.fchmod, réservés à POSIX, afin que les écritures et les outils optionnels fonctionnent sous Windows (#15, #23)Docs et chaîne d'approvisionnement
safe_write_bytes / safe_write_text), refuse les destinations sous forme de liens symboliques, et crée des sauvegardes .bak via le même chemin sûr — les liens symboliques préplacés (par ex. dans /tmp ou les répertoires de téléchargement) ne peuvent plus rediriger une écriture propre vers un fichier arbitrairerewrite_text.py : les redirections sont refusées d'emblée, afin qu'une clé API dans l'en-tête Authorization ne 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-remote ou WATERMARKS_REWRITE_ALLOW_REMOTE=1) ; seuls les schémas http(s) sont acceptés ; --api-key a été supprimé — les clés passent uniquement par l'environnement via WATERMARKS_REWRITE_API_KEYrewrite_text.py effectue 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--tactic humanize : passe zero-shot « écrire comme un humain » ciblant les formulations IA stéréotypées--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--temperature (par défaut 0.9) pour les backends Ollama et OpenAI-compatible--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 longueurSKILL.md, removal-matrix.md et vendor-notes.md ; les tests couvrent les nouveaux prompts, le scoring de divergence et la sélection de candidatsaloshdenny/reverse-SynthID (score_synthid.py) ; exposé dans inspect_image.py / clean_image.py avec REVERSE_SYNTHID_DIR ou --synthid-dirsetup_synthid.sh (dépendances du scorer uniquement ; --full installe les requirements upstream) ; Dockerfile.synthid plus make docker-synthid-build / docker-synthid-helpsmoke-synthid et bootstrap-synthidimage_meta.py : has_manifest ne signale plus Error: No claim found / No JUMBF data found comme un manifeste (bug de priorité d'opérateurs : les marqueurs négatifs annulent désormais toutes les branches positives)tests/test_c2patool_report.py (4 cas : pas de claim, pas de JUMBF, manifeste authentique, outil absent)c2patool corrigés (dépôt déplacé vers contentauth/c2pa-rs) ; ajout d'une mise en garde sur le coût en qualité de la suppression de watermark textuelMakefile (test / smoke / install-skill) et pytest.iniremove-ai-marks (remplace remove-claude-marks, réservé à Claude)inspect_text / clean_text)rewrite_text.py optionnel (print-prompt, Ollama, OpenAI-compatible)inspect_file.py / clean_file.py unifiésc2patool / exiftool optionnelsMIT — voir LICENSE.
| 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 |
| 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) |
| 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", ...}}, ...]} |
| 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 |
| 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 |
rewrite_text.py--strategyCfU+180FU+3164U+FFA0U+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-glue les supprime toujours partoutrewrite_text.py --gumbel-keyWATERMARKS_GUMBEL_KEYgumbel/capabilities/detectparaphrase:3 ; le rapport et le CSV portent les tentatives par document (colonnes mean_attempts / att, attempts / evaluator / passed) ; --rewrite-loops reflète --max-loops--skill--list--linkCLAUDE_CONFIG_DIRcoworkdist/<skill>.zipmakeinstall-claude-code-skillinstall-claude-code-text-skillinstall-claude-project-skillpackage-cowork-skillpackage-cowork-text-skillPostToolUse (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 ; clean les 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ètre hook_mode du plugin ou de WATERMARKS_HOOK_MODE ; la détection réutilise audit_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 mieuxclean-user-facing-text ne nomme plus Cursor comme seul hôteservice/Dockerfile) : service de nettoyage complet avec exiftool / qpdf / c2patool préinstallés ; toute CLI reste exécutable en surchargeant la commandecompose.yaml monte toute l'infrastructure (core toujours ; markllm / markdiffusion derrière profile: harness ; ctrlregen / synthid derrière profile: heavy en tant que builds locaux uniquement) ; les services sont préfixés wr- ; les services harness/heavy ont par défaut command: ["--help"] afin que docker compose up --profile harness --profile heavy se termine proprement (les CLI one-shot sont exécutées avec docker compose run) ; nouveaux make compose-check / compose-check.sh valident la pile en cours d'exécution (code de sortie uniquement).github/workflows/release-images.yml publie les images core, markllm, markdiffusion sur les tags v* ; ctrlregen / synthid ne sont jamais publiés (licence amont).env.example + guide de configuration du service ; docker compose charge automatiquement .env ; .env est gitignoré (refus par défaut).gitignore et service/.dockerignore sont 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 que service/scripts/, ce qui correspond à tout ce que les Dockerfiles COPY)tests/test_http_server.py (13 cas) pour le service HTTP ; toutes les suites sont redirigées vers service/scripts/smoke-markdiffusiondocker-markdiffusion-builddocker-markdiffusion-helptests/test_markdiffusion_harness.py) — pas de torch en CI ; document de référence references/markdiffusion.mdremoval-matrix.md, markdiffusion.mddocker-markllm-help--offline (pas de sortie HF, pas de code distant), plafond de configuration de 1 MiB, WATERMARKS_MARKLLM_RLIMIT_AS optionnel sur le sous-processus de réécriture, torch épinglé dans le Dockerfile, et vérification du SHA de clone dans Dockerfile.markllmtests/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.mdDockerfile.markdiffusioncommon.py/appC2PA, XMP, EXIF et profil ICC (#37)NETSCAPE2.0 est 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--force-text force le passage (#24)--json ne supprime plus le code de sortie de signal résiduel (#30)inspect_file affiche le nom du fichier dans sa sortie (#50)CtrlRegenEngineRLIMIT_ASRLIMIT_FSIZEpermissions: contents: read, dépendances de dev épinglées (requirements-dev.txt), une étape pip-audit, et un nouveau workflow CodeQL ; l'image Docker s'exécute désormais en tant qu'utilisateur non privilégié avec pip épinglé