
watermarks-remover v0.5.0
Una aplicación que prioriza la privacidad y elimina las marcas de agua de IA del contenido que te pertenece.
_ _ _ ____ ___ ____ ____ _ _ ____ ____ _ _ ____ ____ ____ _ _ ____ _ _ ____ ____
| | | |__| | |___ |__/ |\/| |__| |__/ |_/ [__ __ |__/ |___ |\/| | | | | |___ |__/
|_|_| | | | |___ | \ | | | | | \ | \_ ___] | \ |___ | | |__| \/ |___ | \
watermarks-remover
Habilidad de agente + servicio Python de biblioteca estándar para eliminar marcas de procedencia de IA de múltiples proveedores de texto y archivos — para privacidad e higiene en contenido que te pertenece. La habilidad es un cliente ligero: controla la maquinaria a través de HTTP, por lo que el host del agente no necesita Python.
| Capa | Objetivo | Cómo |
|---|---|---|
| A | Unicode invisible, espacios exóticos, bidi, caracteres de etiqueta | Scripts deterministas de Python |
| B | Marcas de agua estadísticas (muestreo de tokens) en texto | Reescritura del agente + hook opcional rewrite_text.py |
| Archivos | C2PA / EXIF / XMP / propiedades de documentos | PNG, JPEG, WebP, AVIF, HEIC, BMP, GIF, TIFF, SVG, PDF, DOCX, XLSX, PPTX, EPUB, ODT, HTML, Markdown, MP4/MOV/M4A/M4V, WAV, MP3, FLAC |
Proveedores / ecosistemas (a nivel de clase): Claude, Gemini / SynthID-Text, superficies de procedencia de OpenAI, marcas open-LLM estilo Kirchenbauer (lista verde) y Gumbel con clave / EXP (Aaronson).
Última versión: v0.7.0
Ruta de la habilidad: skills/remove-ai-marks/
Ruta del servicio: service/
(migración: anteriormente remove-claude-marks; el alias de barra /remove-claude-marks sigue documentado)
Instalación (habilidad de agente)
La habilidad no incluye código — llama al servicio a través de HTTP. Instala la habilidad (solo markdown) e inicia el servicio, luego configura WATERMARKS_SERVICE_URL si no es http://127.0.0.1:8765.
En Claude Code, la ruta más rápida es el marketplace de plugins incluido — sin clonar, y se actualiza en el lugar. En cualquier otro lugar, un instalador cubre todos los hosts compatibles (Python 3.10+ biblioteca estándar, sin dependencias):```bash python3 install_skill.py --skill remove-ai-marks --target claude-code
| Host | Target | Lands in |
| --- | --- | --- |
| Claude Code (personal) | `--target claude-code` | `~/.claude/skills/<skill>` (honors `CLAUDE_CONFIG_DIR`) |
| Claude Code (project) | `--target claude-project --project-dir PATH` | `PATH/.claude/skills/<skill>` |
| Cowork, claude.ai, cloud sessions, routines | `--target cowork` | `dist/<skill>.zip` to upload under **Customize → Skills** |
| Cursor | `--target cursor` (default) | `~/.cursor/skills/<skill>` |
Shipped skills: `remove-ai-marks` (full, service-backed) and
`clean-user-facing-text` (text only, self-contained). `--list` prints them.
Existing installations are preserved unless you pass `--force`; replacement is
staged first and the previous install is kept as a uniquely named backup.
`--link` symlinks this checkout instead of copying, so edits are picked up
live. On Windows, use `py install_skill.py ...`; the `install-skill.sh` wrapper
is provided for macOS/Linux shells.
Before writing anything, the installer validates the skill against the
[Agent Skills](https://agentskills.io) packaging rules that claude.ai uploads
and the Skills API enforce: spec-only frontmatter (`name`, `description`,
`license`, `compatibility`, `metadata`, `allowed-tools`), a lowercase hyphenated
`name` of at most 64 characters matching the directory, a non-empty
`description` of at most 1024 characters. The Cowork bundle additionally has
to fit the 30 MB upload limit, which the packager enforces.
### Limpieza automática mediante hook (determinista)
Una skill es una instrucción: el modelo decide si invocarla, y el
modelo es lo que produce las marcas. Un **hook** es ejecutado por el harness
en cada llamada a herramienta que coincida, sin necesidad de cooperación. Eso hace del hook la
mitad determinista de este flujo de trabajo.
El plugin registra un hook `PostToolUse` sobre `Write|Edit|MultiEdit|NotebookEdit`
que ejecuta [`service/scripts/hook_written_file.py`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/service/scripts/hook_written_file.py)
contra el archivo que el agente acaba de escribir. Dos modos, que coinciden con la convención de pre-commit de comprobar por defecto:
| Modo | Comportamiento |
| --- | --- |
| `check` (default) | Reporta las marcas de procedencia, deja el archivo intacto. Los hallazgos van al modelo (exit 2), para que pueda ofrecerse a limpiarlos. |
| `clean` | Elimina las marcas in situ, luego informa al modelo que el archivo en disco cambió. |
Establece el modo desde la configuración del plugin (**Hook mode** en `/plugin manage`,
leído por el hook como `CLAUDE_PLUGIN_OPTION_HOOK_MODE`), o con
`WATERMARKS_HOOK_MODE=clean` en el entorno. El comando del hook deliberadamente
**no** interpola `${user_config.hook_mode}`: Claude Code se niega a ejecutar
un hook que hace referencia a una opción que el usuario nunca ha abierto en `/plugin manage` para
establecer — un `default` declarado no lo satisface — así que interpolarlo significaría
que el hook silenciosamente nunca se ejecutaría en una instalación nueva. La detección reutiliza `scan_file` / `is_actionable` de `audit_lib`,
así que el hook, la puerta de pre-commit y la exportación SARIF de CI coinciden en qué cuenta como accionable; la limpieza invoca a
`clean_file.py`, así que no se duplica lógica de limpieza. El modo `clean` escribe en un
archivo temporal hermano y solo lo intercambia ante una diferencia real, así que los archivos que ya
estaban limpios conservan su mtime y no vuelven a activar los observadores de archivos.
Sin el plugin, conéctalo tú mismo en `~/.claude/settings.json` (o en un
`.claude/settings.json` de proyecto):```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
}
]
}
]
}
}
En Windows, reemplaza python3 por py.
Lo que un hook no puede hacer. Ningún hook puede reescribir el mensaje de chat del asistente
antes de que lo leas. El hook Stop de Claude Code recibe last_assistant_message
como solo lectura, y no existe un filtro previo al envío para las respuestas finales — el mismo límite
que este proyecto ya documenta para las reglas de Cursor. Por lo tanto, la garantía
determinista cubre los archivos que el agente escribe, más la
puerta de pre-commit para cualquier cosa que esté en camino a git. El texto que
solo existe en la transcripción del chat todavía depende del flujo de trabajo de la skill,
que está basado en instrucciones del modelo y por lo tanto es de mejor esfuerzo.
Plugin de Claude Code (marketplace)
El repositorio también es un plugin de Claude Code y un
marketplace de un solo plugin (.claude-plugin/), por lo que ambas skills se instalan y actualizan en dos
comandos, sin necesidad de clonar ni de scripts:```
/plugin marketplace add guillaumemeyer/watermarks-remover
/plugin install watermarks-remover@watermarks-remover
Las habilidades luego se cargan con espacio de nombres: `/watermarks-remover:remove-ai-marks` y
`/watermarks-remover:clean-user-facing-text` (el simple `/remove-ai-marks` también
funciona cuando nada más reclama el nombre). `/plugin marketplace update
watermarks-remover` obtiene versiones posteriores. Lo mismo funciona desde la CLI con
`claude plugin marketplace add …` / `claude plugin install …`, y desde un checkout local
pasando una ruta en lugar de `owner/repo`.
Mantenedores: `make plugin-validate` ejecuta `claude plugin validate . --strict`
contra ambos manifiestos; `tests/test_plugin_manifest.py` cubre los mismos archivos
sin necesidad 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 recoge las habilidades personales y de proyecto sin reiniciar; /skills
muestra lo que ha cargado. Invócalo con /remove-ai-marks o pide que “elimine las
marcas de IA / C2PA / marcas de Claude / texto de clase SynthID.” Una instalación de proyecto es
también lo que leen las sesiones en la nube,
ya que clonan el repositorio y cargan su .claude/skills/.
Cowork (y claude.ai, sesiones en la nube, rutinas)
Las sesiones de Cowork no leen ~/.claude/skills en tu máquina — cargan
las habilidades habilitadas para tu cuenta de claude.ai, sincronizadas cuando comienza la sesión.
Así que instala allí subiendo un paquete:```bash
python3 install_skill.py --skill remove-ai-marks --target cowork
writes dist/remove-ai-marks.zip (make package-cowork-skill)
Luego, en la aplicación Claude Desktop, abre **Customize → Skills → Add** y sube
el zip (la misma configuración de skills en claude.ai también funciona). El paquete es
reproducible y contiene un único directorio de nivel superior `remove-ai-marks/` con
`SKILL.md` en su raíz, que es la estructura que espera la carga.
La accesibilidad del servicio importa más aquí que en una instalación local: la skill es un
cliente HTTP ligero, por lo que la sesión debe poder alcanzar `WATERMARKS_SERVICE_URL`.
Las sesiones de Cowork que se ejecutan localmente en tu máquina alcanzan un `make serve` local;
las sesiones en la nube y las rutinas se ejecutan de forma remota y necesitan una URL de servicio accesible desde
allí (y `WATERMARKS_SERVER_API_KEY` configurada en ella). Si quieres una skill sin
servicio alguno, sube `clean-user-facing-text` en su lugar — es solo texto y
incluye sus propios 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
### Habilidad opcional solo de texto
[`skills/clean-user-facing-text/`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/skills/clean-user-facing-text) es una
habilidad autocontenida para manuscritos autorizados, documentación y textos
web. Excluye herramientas de imagen, C2PA, servicios y modelos externos, y ejecuta
sus propios scripts de Capa A integrados en lugar de llamar al servicio.```bash
python3 install_skill.py --skill clean-user-facing-text --target claude-code
python3 install_skill.py --skill clean-user-facing-text --target cursor
La invocación de habilidades es seleccionada por el modelo. Los proyectos que adoptan explícitamente este flujo de trabajo en Cursor también pueden copiar la regla opcional:```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
Para todos los proyectos, coloca la misma instrucción en las **User Rules** de Cursor en su lugar.
Las reglas mejoran la consistencia pero siguen siendo instrucciones para el modelo; Cursor no expone
un filtro determinista previo al envío para las respuestas finales del chat.
### Iniciar el servicio
La vía más rápida es un servidor HTTP local (solo stdlib de Python 3.10+ — sin dependencias, sin 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 (sin Docker)
Consulta docs/windows-autostart.md para iniciar automáticamente el servicio al iniciar sesión en Windows sin Docker.
Para toda la infraestructura (core + backends opcionales harness/heavy), consulta Docker / compose a continuación.
Herramientas de sistema opcionales (se usan automáticamente cuando están presentes — preinstaladas en la imagen Docker core):
| Herramienta | Rol |
|---|---|
c2patool | Inspeccionar manifiestos C2PA |
exiftool | Eliminación de metadatos residuales (esp. PDF) |
qpdf | Reconstrucción estructural de PDF — requerido para una eliminación real de PDF (ver abajo) |
Los scripts core solo necesitan la stdlib de Python 3.10+. Las llamadas al modelo de Capa B son opcionales.
Uso rápido (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
### Las herramientas de texto rechazan la entrada binaria
`inspect_text.py`, `clean_text.py` y `rewrite_text.py` operan sobre texto. Al apuntarlas
a un `.docx`, `.pdf` o imagen, solían decodificar los bytes comprimidos y reportar
los puntos de código que aparecieran — ruido que refleja la compresión, no el
contenido — y luego `clean_text.py` volvía a escribir esos bytes alterados, destruyendo el
archivo. Ahora rechazan la entrada binaria e indican la herramienta que lo
maneja:```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 detección se realiza mediante número mágico más una proporción de bytes de control, por lo que el texto en codificaciones distintas de UTF-8 sigue funcionando. --force-text lo anula en todas partes.
Los formatos no reconocidos nunca se limpian automáticamente
classify() etiqueta los bytes que no coinciden con ningún formato de texto, imagen o contenedor compatible como unknown — ya no recurre a "text". En modo automático clean_file.py rechaza dichos archivos (salida 2, no se escribe ninguna salida) en lugar de decodificarlos como UTF-8 y reescribir bytes alterados; --as text o --force-text son las opciones explícitas. inspect_file.py informa el archivo como unknown (salida 0), y el servicio HTTP responde a /inspect con kind: "unknown" pero rechaza /clean de formatos desconocidos (400 — envíe un nombre de archivo con una extensión conocida, p. ej. notes.txt).
Servicio HTTP
La misma maquinaria se ejecuta como un servicio HTTP de la biblioteca estándar (service/scripts/server.py) — la interfaz que utiliza la skill y la forma en que cualquier aplicación web puede integrarse sin vendorizar:
| Método | Ruta | Cuerpo | Devuelve |
|---|---|---|---|
| GET | /health | — | {"ok": true, "version": ...} |
| GET | /capabilities | — | herramientas / backends opcionales utilizables (cada herramienta se sondea por versión, no solo se busca en PATH) |
| GET | /openapi.json | — | especificación OpenAPI 3.0.3 generada dinámicamente |
| POST | /inspect | {"file": "<base64>", "name": "notes.md"} | {"ok", "kind", "suspicious", "report"} |
| POST | /detect | {"file": "<base64>", "name": "notes.txt"} | {"ok", "kind", "detections": [...]} |
| POST | /clean | {"file": "<base64>", "name": "notes.md", "options": {...}} | {"ok", "kind", "cleaned": "<base64>", "report"} |
| POST | /watermark | {"text": "...", "keys": [118, 504, ...], "options": {...}} o {"file": "<base64>", ...} | {"ok", "kind", "watermarked_text", "report": {"scheme_used", ...}} |
| POST | /inspect/batch | {"files": [{"file": "<base64>", "name": "notes.md"}, ...]} | {"ok", "results": [{"name", "ok", "kind", "suspicious", "report"}, ...]} |
| POST | /detect/batch | {"files": [{"file": "<base64>", "name": "notes.txt"}, ...]} | {"ok", "results": [{"name", "ok", "kind", "detections", "report"}, ...]} |
| POST | /clean/batch | {"files": [{"file": "<base64>", "name": "notes.md", "options": {...}}, ...]} | {"ok", "results": [{"name", "ok", "kind", "cleaned", "report"}, ...]} |
| POST | /watermark/batch | {"files": [{"text": "...", "keys": [...]}, {"file": "<base64>"}, ...]} | {"ok", "results": [{"name", "ok", "kind", "watermarked_text", "report": {"scheme_used", ...}}, ...]} |
Los endpoints por lotes recorren el mismo pipeline por archivo que /inspect, /detect, /clean y /watermark, con un límite de WATERMARKS_MAX_BATCH_FILES archivos por solicitud (50 por defecto). Una entrada malformada (base64 incorrecto, opción desconocida, formato no reconocido) se muestra como "ok": false de esa entrada con una cadena "error" — nunca aborta el resto del lote.```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"}"
El servicio enruta por extensión de archivo y luego por bytes mágicos, por lo que texto / imagen / contenedor se detectan automáticamente. Establece `WATERMARKS_SERVER_API_KEY` para requerir `Authorization: Bearer <key>` en cada solicitud. Enlace solo a loopback por defecto (`--host` para sobrescribir); pensado para una red de confianza.
### Detección de marcas de agua (`/detect` y `detect_before` / `detect_after`)
La detección es un paso separado de la limpieza — el servicio nunca llama a APIs
de proveedores a menos que se lo pidas:
- **`POST /detect`** ejecuta los detectores de marcas de agua configurados sobre un archivo.
Texto → detectores de proveedor + estilometría; imagen → puntuación de píxeles de SynthID.
- **`/inspect`** acepta un flag opcional `"detect": true` que añade
los resultados del detector al informe de texto (y puede cambiar `suspicious`).
- **`/clean`** acepta las opciones `"detect_before"` / `"detect_after"` para
puntuar la entrada y la salida limpiada, de modo que puedas medir qué cambió
realmente una limpieza.
- **`/clean`** ejecuta la reescritura de texto de la Capa B después de la Capa A **por defecto** (es
un paso obligatorio para texto). Una opción **`"strategy"`** (una lista ordenada
`tactic@intensity`, p. ej. `"[email protected],[email protected]"`) sobrescribe el
valor por defecto del archivo de configuración de estrategia (ver más abajo). Cuando el backend/modelo de reescritura
para un paso no está configurado, `/clean` devuelve un 400.
Detectores de texto (ver `/capabilities` → `text_detectors`):
Detectores de texto (ver `/capabilities` → `text_detectors`):
| Detector | Activado por | Notas |
| --- | --- | --- |
| `markllm` | `MARKLLM_DIR` (checkout local) | Arnés de investigación (esquemas KGW / SynthID), solo misma configuración — no es un oráculo de proveedor. |
| `gumbel` | `WATERMARKS_GUMBEL_KEY` | Reproducción sin modelo con la misma clave del esquema keyed-Gumbel (Aaronson EXP) (ver `detect_gumbel.py`), solo stdlib — motores autoalojados como arbi-serve; solo misma clave, no es un oráculo de proveedor. |
| `claude-text` | — (marcador de posición) | Anthropic ha anunciado una API de detección de marcas de agua; esta interfaz se activará cuando se lance. |
Puntuación de imágenes: cuando `WATERMARKS_SYNTHID_SCORER_URL` está establecido, el servicio
puntúa las imágenes a través del sidecar `wr-synthid-score` (perfil heavy); con un
`REVERSE_SYNTHID_DIR` local usa el checkout directamente. La detección es fail-soft: los detectores no configurados, con timeout o con error informan
`{"available": false, "error": ...}` y nunca bloquean la limpieza.
### Generación de marcas de agua (`/watermark` y `/watermark/batch`)
Genera texto con marca de agua para evaluación de benchmarks y pruebas de ida y vuelta.
Cuando `WATERMARKS_SYNTHID_TEXT_URL` está establecido, el servicio delega la generación al
sidecar `wr-synthid-text` (perfil harness); con un `MARKLLM_DIR` local usa el
checkout directamente. Al igual que la detección, la generación es fail-soft: un generador no configurado
informa `{"ok": false, "error": ...}`.
## Docker / compose
Imágenes publicadas (GHCR):
| Etiqueta de imagen | Contenido | ¿Publicada? |
| --- | --- | --- |
| `ghcr.io/guillaumemeyer/watermarks-remover:<tag>` / `:latest` | Servicio HTTP principal + todos los limpiadores + exiftool / qpdf / c2patool | Sí |
| `…:markllm-<tag>` / `:markllm-latest` | Arnés de marcas de agua de texto MarkLLM (upstream Apache-2.0) | Sí |
| `…:markdiffusion-<tag>` / `:markdiffusion-latest` | Arnés de imágenes MarkDiffusion (upstream Apache-2.0) | Sí |
| `watermarks-remover-ctrlregen:local` | Eliminación de píxeles CtrlRegen — **nunca publicada** (`noai-watermark` no incluye LICENSE) | Solo compilación local |
| `watermarks-remover-synthid-scorer:local` | Puntuador reverse-SynthID — **nunca publicada** (Licencia de Investigación no comercial) | Solo compilación local (puntuador CLI + sidecar HTTP `wr-synthid-score` opcional bajo el perfil `heavy`) |
Compilar y ejecutar el servicio 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
Puesta en marcha de toda la infraestructura:```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
El stack de compose mapea el servicio principal a `127.0.0.1:8765`. Los servicios persistentes se ejecutan como demonios en segundo plano (`wr-core` y el sidecar `wr-synthid-text` bajo el perfil de harness). Los servicios restantes de harness/pesados son CLIs de un solo uso — invócalos con `docker compose run --rm <service> …` cuando necesites verificación o trabajo de píxeles.
Valida el stack en ejecución (solo código de salida, sin salida en caso de éxito):```bash
make compose-check # or: ./compose-check.sh
Comprueba wr-core mediante GET /health y ejecuta cada harness/servicio pesado con --help, requiriendo salida 0.
Configuración (variables de entorno para docker compose)
La limpieza de texto requiere la configuración de la Capa B — la reescritura de la Capa B es un
paso obligatorio para POST /clean en texto, por lo que el servicio core necesita el backend de reescritura
configurado, o la limpieza de texto devuelve HTTP 400. La limpieza de metadatos de imagen/contenedor
funciona sin configuración adicional. Para texto debes configurar las dependencias de la estrategia de la Capa B: transformers + roberta-large (para el paso mlm por defecto) y
la configuración LLM WATERMARKS_REWRITE_* (para el paso 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"}"
Los idiomas cuya tipografía depende de un espacio no separable (francés `« … »`, el
espacio antes de `; : ! ?`) deben pasar `"options": {"normalize_spaces": false}`, el
equivalente HTTP de `clean_text.py --no-normalize-spaces`. Los portadores invisibles
siguen eliminándose; solo se omite la reescritura de espacios.
Todo lo demás es opcional y reside en un archivo `.env` en la raíz del repositorio. `docker compose` **carga automáticamente `.env`** e interpola las referencias `${VAR}` en `compose.yaml` a partir de él (las exportaciones del shell tienen prioridad sobre `.env` si ambos están definidos).```bash
cp .env.example .env # then edit
docker compose up -d # picks up .env automatically
.env está gitignored (denegado por defecto) — nunca lo confirmes. Para ejecuciones de CLI en el host (rewrite_text.py, la skill), exporta el mismo archivo al entorno:```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 Capa B es obligatoria para la limpieza de texto.** `/clean` siempre aplica la estrategia predeterminada (desde `config/clean_strategy.json`, `{"default_strategy": "[email protected],[email protected]"}`) a un archivo de texto después de la Capa A, a menos que la solicitud pase su propia opción `"strategy"` (una lista ordenada de `tactic@intensity`). Un paso de estrategia es `tactic@intensity`; el paso `mlm` necesita `transformers` + `roberta-large`, y cualquier paso LLM (`paraphrase`, `humanize`, …) necesita la configuración `WATERMARKS_REWRITE_*`. Si el backend/modelo requerido no está configurado — o no hay ninguna estrategia disponible — `/clean` **rechaza la solicitud con un 400**. Precedencia para la ruta de configuración: flag CLI `--strategy-config` > variable de entorno `WATERMARKS_CLEAN_STRATEGY_FILE` > el valor predeterminado `config/clean_strategy.json`.
Las imágenes se publican automáticamente en las etiquetas `v*` mediante [`.github/workflows/release-images.yml`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/.github/workflows/release-images.yml).
## Puntuación opcional de píxeles SynthID
`inspect_image.py` y `clean_image.py` pueden informar una puntuación de confianza SynthID en el dominio de píxeles cuando está disponible una copia externa de
[`aloshdenny/reverse-SynthID`](https://github.com/aloshdenny/reverse-SynthID). El scorer **no está incluido**: se carga en tiempo de ejecución desde tu copia, y su código permanece bajo la Licencia de Investigación no comercial del proyecto upstream.
### Opción 1: bootstrap con un solo comando (sin 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 acepta --dir PATH, --ref REF y --full (instala el
requirements.txt completo del upstream, lo que añade torch/diffusers para
el bypass del VAE del upstream que este proyecto no utiliza).
En Windows use setup_synthid.ps1 (-Dir, -Ref, -Full), que crea el
venv en .venv\Scripts\ — la estructura que image_meta.py ya busca en
os.name == "nt".
Opción 2: compilación local con Docker```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
La imagen se construye localmente a partir del código fuente upstream en el momento de la compilación. No se publica, por lo que no redistribuye el código upstream.
### Opción 3: sidecar del scorer HTTP (docker compose)
Bajo el perfil `heavy`, el stack de compose también ejecuta el scorer como un sidecar HTTP (`wr-synthid-score`) para que el **servicio core publicado** pueda puntuar imágenes antes/después de la limpieza sin incluir el código upstream no comercial. Apunta `wr-core` a él y comparte una clave bearer (ver `.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
Luego, POST /clean con {"options": {"detect_before": true, "detect_after": true}} devuelve synthid_before / synthid_after en el
informe, y POST /detect sobre una imagen devuelve la puntuación de SynthID. Fail-soft:
si el sidecar está caído o no configurado, los informes incluyen
{"available": false, "error": ...} y la limpieza sigue teniendo éxito.
La puntuación V4 utiliza artifacts/spectral_codebook_v4.npz del checkout upstream
(`220 MB). Esto es solo detección/puntuación — no elimina marcas de agua
a nivel de píxel.
Eliminación opcional de píxeles con CtrlRegen
Para marcas de agua de imagen en el dominio de píxeles (clase SynthID, StegaStamp, Tree-Ring,
StableSignature), un backend externo opcional ejecuta el pipeline de CtrlRegen
(regeneración controlable con ControlNet + DINOv2 IP-Adapter). El backend es
mertizci/noai-watermark, una
reimplementación mantenida del método ICLR 2025
CtrlRegen con tiling automático.
El backend no está incluido y no incluye ningún archivo LICENSE, por lo que se trata como
todos los derechos reservados: se clona en un commit fijado y se carga en tiempo de ejecución.
Sus dependencias fijadas de la era de investigación (requirements-ctrlregen.txt — por ejemplo,
transformers==4.37.2, diffusers==0.27.2) conllevan avisos publicados y
no están actualizadas intencionadamente, por lo que solo se instalan dentro del
venv dedicado que crea este script y nunca en la imagen principal del servicio;
setup_ctrlregen.sh también vuelve a verificar el commit fijado en checkouts
existentes, no solo en clones nuevos.
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
En Windows usa `setup_ctrlregen.ps1` (mismos flags que `-Dir`, `-Ref`, `-Python`);
el venv queda en `.venv\Scripts\`, que `clean_image.py` ya resuelve.
Sondea los índices publicados de wheels de PyTorch y elige el más alto que sea igual o
inferior a la versión de CUDA que imprime `nvidia-smi` y que realmente exista — ese número
es el máximo que soporta el *driver*, y los drivers son retrocompatibles, así que un
driver que reporta 13.1 (sin `cu131` publicado) instala `cu130`. Por debajo de la capacidad
de cómputo 7.5 fuerza `cu126`, el último índice cuyos wheels todavía incluyen kernels de
Maxwell/Pascal/Volta. Instala `torch` **y** `torchvision`
juntos desde ese índice para que la instalación de dependencias no pueda cambiarlos por
builds de CPU desde PyPI, y luego verifica tras la instalación que `torch.cuda.is_available()`
sea true — si se detectó una GPU pero torch termina siendo solo CPU, el script advierte
ruidosamente y sale con código distinto de cero en lugar de pretender que la configuración tuvo éxito.
### Desde `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
Orden de operaciones: primero la eliminación de metadatos, luego la eliminación de píxeles de CtrlRegen, y después
una puntuación opcional de reverse-SynthID antes/después (cuando REVERSE_SYNTHID_DIR también está
configurado).
La intensidad es conservadora por defecto (--ctrlregen-intensity 0.25), porque
una intensidad mayor elimina más marca de agua pero regenera más de la imagen.
Presets documentados: 0.15 mínimo / 0.25 por defecto / 0.35 equilibrado /
0.5 agresivo / 0.7 máximo (el valor por defecto del backend es 0.5). --ctrlregen-steps
por defecto es 50 (pasos de eliminación de ruido efectivos ≈ pasos × intensidad).
Tamaño de imagen (límite nativo de 512×512)
CtrlRegen es un ControlNet de Stable Diffusion 1.5 de 512×512. El backend resuelve esto para entradas arbitrarias, por lo que no se expone ningún mosaico adicional aquí:
- ≤512 px: una sola pasada — recorte central/redimensionar a 512, regenerar, redimensionar de vuelta.
- >512 px: mosaico superpuesto automático (mosaicos de 512 px, superposición de 192 px), ancho/alto alineados a múltiplos de 8, luego uniones con mezcla coseno.
- Cualquiera de las dos rutas: la salida se redimensiona al tamaño original y se iguala el color a la imagen original.
Las imágenes muy grandes (p. ej. 4K) producen muchos mosaicos, por lo que las ejecuciones escalan con el número de mosaicos (más lentas y con mayor uso de VRAM). Reduzca previamente las entradas grandes cuando sea práctico; el tamaño del mosaico y la superposición están codificados en el upstream y no se exponen como flags.
Cómputo, modelos con acceso restringido y verificación
Espere ~10 GB de descargas de modelos; se recomienda encarecidamente una GPU y las ejecuciones en CPU
son lentas. Algunos modelos del upstream tienen acceso restringido, así que exporte HF_TOKEN (solo como variable de entorno —
nunca en argv). clean_ctrlregen.py se niega a instalar dependencias automáticamente; ejecute
setup_ctrlregen.sh primero.
No hay un detector local para StegaStamp/Tree-Ring/StableSignature, por lo que la
única señal local es la puntuación de reverse-SynthID (un sustituto). Cuando esté disponible,
clean_image.py --remove-pixel ctrlregen informa esa puntuación antes/después; la
verificación oficial de Google SynthID sigue siendo la autoridad final.
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
## Verificación opcional de marcas de agua de texto con MarkLLM
Para **experimentos controlados**, un arnés externo opcional envuelve
[`THU-BPM/MarkLLM`](https://github.com/THU-BPM/MarkLLM) (Apache-2.0) para
marcar con marca de agua texto de prueba y volver a detectarla tras una reescritura de Capa B — por ejemplo, demostrar que
una marca KGW (Kirchenbauer, tu fila "open-LLM") o SynthID-Text (fila Gemini) desaparece bajo tu reescritura. Es un **arnés de verificación, no un oráculo**:
la detección de MarkLLM solo es válida contra la *misma* configuración de esquema + claves usadas en
la generación, y no puede certificar que un detector de proveedor vaya a fallar.
El backend **no está incluido**. `setup_markllm.sh` clona el upstream en un commit fijado, crea un venv e
instala dependencias fijadas (torch + transformers); el
modelo de puntuación (por defecto `facebook/opt-1.3b`, Apache-2.0) se descarga de Hugging
Face en la primera ejecución.```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
Verificación en torno a una reescritura de Capa B: pasa --markllm-scheme a
rewrite_text.py (con --markllm-dir), y registra la detección de MarkLLM
antes/después más una bandera 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
**Reescritura iterativa guiada por detección:** La Capa B ahora reescribe de forma iterativa y
se detiene en cuanto un intento pasa la evaluación. Cada ronda de evaluación genera
variantes `--candidates` (por defecto **1**, `WATERMARKS_REWRITE_CANDIDATES`)
y `--max-loops` limita cuántas rondas se ejecutan antes de devolver la variante de mejor esfuerzo
(por defecto **1**, `WATERMARKS_REWRITE_LOOPS`). Cada variante es una
llamada de reescritura más una evaluación, y una ronda termina anticipadamente en el primer intento
que el evaluador informa como no marcado con marca de agua — por lo que aumentar `--max-loops` reintenta
nuevas variantes hasta que una evaluación pase (una reescritura limpia típica cuesta un
intento). El evaluador se elige por prioridad:
1. **MarkLLM** — detección de investigación con la misma configuración, cuando se pasa
`--markllm-scheme` (con `--markllm-dir`). Se reserva una ranura para detector de proveedor
por encima de MarkLLM para el detector SynthID-text de Google, que Google retiró de su API
en agosto de 2026 — un futuro endpoint de proveedor puede conectarse ahí.
2. **divergencia léxica bigram-Jaccard** — cuando no hay ningún detector configurado; sin
veredicto de aprobado/reprobado, por lo que se genera cada intento y se selecciona el más
divergente léxicamente (el comportamiento original).
`--json-stats` informa el evaluador, los intentos realizados, aprobado/reprobado y los registros
por intento:```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 detector que no está configurado, agota el tiempo de espera o produce un error genera una entrada "available": false con un motivo error y nunca hace fallar la reescritura — ese intento simplemente no puede pasar, y el bucle recurre a la selección por divergencia léxica. Cuando se agota el máximo sin un intento exitoso, se devuelve el intento menos marcado (puntuación más baja) como mejor esfuerzo con una nota.
Si el backend no está configurado o faltan sus dependencias, la reescritura continúa y el informe indica que la verificación no estaba disponible. Se recomienda una GPU; las ejecuciones en CPU funcionan pero son lentas, y la descarga del modelo es de unos pocos GB.
Ajustes de endurecimiento:
--offlineen el adaptador (o en cualquier ejecución de MarkLLM) carga el modelo de puntuación solo desde la caché de Hugging Face — sin salida de red; falla rápidamente si no está en caché. El código remoto personalizado nunca se ejecuta (eltrust_remote_codede transformers nunca se habilita).WATERMARKS_MARKLLM_RLIMIT_AS=<bytes>(variable de entorno, POSIX) aplica un límite de espacio de direcciones al subproceso del detector de MarkLLM. Desactivado por defecto porque torch/CUDA normalmente necesita espacios de direcciones grandes.- Los archivos de configuración están limitados a 1 MiB; el checkout upstream y la imagen base están fijados por 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
### Verificación de misma clave de Keyed-Gumbel (Aaronson EXP)
El [informe técnico de ARBI](https://arbicity.com/news/ai-text-watermarking-for-self-hosted-ai/) describe la
marca de agua de texto keyed-Gumbel ("exponencial") — ahora incluida en el motor
de código abierto arbi-serve (`ARBI_WATERMARK_KEY`) — donde el ruido del muestreador
se deriva de un hash con clave de la ventana de contexto de los últimos 4 tokens. La detección es una
**reproducción sin modelo**: recalcular `u = PRF(Hash(key, window), token)` a partir del
texto únicamente y probar la cola Gamma, por lo que no necesita GPU, modelo ni logits.
Este repositorio incluye ese detector como `detect_gumbel.py` (solo biblioteca estándar; el valor p
es la identidad exacta de suma de Poisson para una forma Gamma entera):```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
Misma advertencia de honestidad que con MarkLLM: esto es una repetición con la misma clave — válida únicamente contra la misma clave, tokenizador y diseño de PRF utilizados en la generación, y un resultado negativo no establece nada. El diseño de HMAC-SHA256 aquí es una instanciación auditable, no compatible a nivel de bits con ningún kernel de motor específico (consulta el docstring del módulo para saber qué adaptar para una repetición exacta).
Reescritura guiada por detección: pasa --gumbel-key a rewrite_text.py
(env: WATERMARKS_GUMBEL_KEY, preferido) y el bucle de reescritura iterativa es
impulsado por la repetición Gumbel con la misma clave — la prioridad del evaluador pasa a ser gumbel >
MarkLLM > divergencia léxica — con un informe 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 clave nunca aparece en las estadísticas ni en los registros. Los operadores autoalojados que poseen la clave de su motor pueden verificar que una reescritura eliminó una marca de Gumbel; todos los demás tratan la Capa B como de mejor esfuerzo únicamente.
## Benchmark opcional de eliminación de SynthID-text
[`bench_synthid_text.py`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/service/scripts/bench_synthid_text.py) mide con qué eficacia una reescritura de Capa B elimina las marcas de agua de la clase SynthID-text y a qué costo. Genera muestras con y sin marca de agua con el esquema SynthID de MarkLLM (detección con la misma configuración, con verificación de cordura), ejecuta tus variantes de reescritura (táctica × intentos máximos de reescritura; el bucle se detiene anticipadamente al aprobar) más los controles (sin eliminación, solo Capa A, verificación opcional de re-sellado), y escribe un `report.md` / `results.json` / `results.csv` compartibles. Guía completa: [`docs/synthid-text-benchmark.md`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/docs/synthid-text-benchmark.md).
Requiere una copia local de MarkLLM (`setup_markllm.sh` / `MARKLLM_DIR`) y un backend de reescritura. **El modelo de reescritura es un LLM que tú configuras** — el mismo backend `rewrite_text.py` que usa la skill. El `facebook/opt-1.3b` predeterminado de MarkLLM (`--markllm-model`) es solo el generador/detector de marcas de agua; nunca reescribe. Configura el modelo de reescritura mediante variables de entorno o flags del benchmark (reflejan la [tabla de configuración](#configuration-env-vars-for-docker-compose) anterior):
| Variable de entorno | Flag del benchmark | Predeterminado | Significado |
| --- | --- | --- | --- |
| `WATERMARKS_REWRITE_BACKEND` | `--rewrite-backend` | `ollama` | `ollama` u `openai-compatible` |
| `WATERMARKS_REWRITE_MODEL` | `--rewrite-model` | *(obligatorio)* | El LLM que realiza la reescritura (p. ej. `llama3.2`, `deepseek-v4-flash`) |
| `WATERMARKS_REWRITE_BASE_URL` | `--rewrite-base-url` | `http://127.0.0.1:11434` | Endpoint; el predeterminado de Ollama es loopback |
| `WATERMARKS_REWRITE_API_KEY` | `--rewrite-api-key` | — | Clave de API (solo por entorno en el proceso hijo, nunca por argv) |
| `WATERMARKS_REWRITE_ALLOW_REMOTE=1` | `--rewrite-allow-remote` | desactivado | Requerido para enviar contenido a endpoints que no sean 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
Utilice un modelo no original para la reescritura (no reescriba con el mismo modelo con marca de agua que generó el texto) o la reescritura puede volver a estampar la salida; --restamp-control mide esto.
Arnés opcional de marcas de agua en imágenes MarkDiffusion
Para experimentos controlados con imágenes, un arnés externo opcional envuelve
THU-BPM/MarkDiffusion (Apache-2.0),
un conjunto de herramientas de marcado de agua generativo para modelos de difusión latente (incrusta marcas
— no las elimina). Lo usamos para tres cosas:
- Arnés de verificación (como MarkLLM, pero para imágenes): marcar una imagen de prueba con un esquema, ejecutar la eliminación y volver a detectar con la misma configuración de esquema — por ejemplo, demostrar que una marca de clase Tree-Ring se elimina bajo su pipeline. Es un arnés de verificación, no un oráculo: la detección requiere el modelo generador (y claves para esquemas basados en claves), por lo que no puede certificar que un detector de proveedor falle en una imagen arbitraria.
- Motor opcional de eliminación de píxeles: su ataque de regeneración
DiffusionPurificationse expone comoclean_image.py --remove-pixel diffusion, una alternativa a CtrlRegen. Es regeneración ciega (sin condicionamiento ControlNet), por lo que desvía el contenido de la imagen más que CtrlRegen — intensidad predeterminada conservadora (0.3), tratada como respaldo/comparación, nunca como garantía. - Detector local del mismo esquema para marcas de clase Tree-Ring, que llena parcialmente la brecha de "sin detector local para StegaStamp/Tree-Ring/StableSignature" (cubre Tree-Ring/Ring-ID/Gaussian-Shading, etc., no StegaStamp / StableSignature / SynthID-media).
El backend no está incluido. setup_markdiffusion.sh crea un venv e
instala markdiffusion==1.0.2 desde PyPI (fijado), con torch instalado desde
el índice de plataforma correcto; --checkout instala un clon editable en un commit
fijado en su lugar. El modelo Stable Diffusion (predeterminado
huanzi05/stable-diffusion-2-1-base) se descarga desde Hugging Face en la primera ejecución.```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
O ejecutar la purificación como parte del pipeline normal de imágenes:```bash
MARKDIFFUSION_DIR=~/markdiffusion \
~/markdiffusion/.venv/bin/python "$SCRIPTS/clean_image.py" shot.png \
-o shot.cleaned.png --remove-pixel diffusion
Los controles de endurecimiento reflejan el arnés de MarkLLM: --offline carga el modelo únicamente desde la caché de Hugging Face (cero egreso de red, sin código remoto), HF_TOKEN es solo por variable de entorno (nunca por argv), las configuraciones de algoritmos están limitadas a 1 MiB, y el subproceso recibe los mismos límites de recursos superiores 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
La imagen instala un torch de CPU; los usuarios de CUDA deberían ejecutar `setup_markdiffusion.sh`
en el host en su lugar. Las descargas de modelos siguen accediendo al hub de HF en la primera ejecución.
## Matriz de cobertura
| Canal | Claude | Gemini/SynthID | OpenAI | Open-LLM |
| --- | --- | --- | --- | --- |
| Texto Unicode / basado en edición | Capa A | Capa A | Capa A | Capa A |
| **Texto de muestreo estadístico** | Capa B best-effort (unión con Claude cuando se lance la API de detección de Anthropic) | Capa B best-effort (+ arnés MarkLLM con misma configuración; Google retiró el detector del proveedor en agosto de 2026) | Capa B si está presente | Capa B best-effort + arnés MarkLLM opcional |
| C2PA / metadatos de archivo | Sí (formatos listados) | Sí cuando está presente | Sí cuando está presente | Sí cuando está presente |
| Marcas de imagen a nivel de píxel | Fuera de alcance | Puntuación SynthID opcional + eliminación con CtrlRegen (externo); detección opcional de mismo esquema con MarkDiffusion + eliminación con DiffusionPurification (externo) | Fuera de alcance | Eliminación opcional con CtrlRegen / MarkDiffusion (externo) |
| Backdoors de entrenamiento | Fuera de alcance | Fuera de alcance | Fuera de alcance | Fuera de alcance |
Detalles: [`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).
---
## Cómo funciona el marcado de texto (breve)
Las marcas de agua modernas de los LLM a menudo ocultan una señal en **qué tokens se eligen** (sesgo generativo / de muestreo), no solo en caracteres invisibles. Los esquemas basados en edición inyectan reglas de Unicode o sinónimos. Los esquemas de archivo adjuntan **C2PA** o metadatos del generador.
- La **Capa A** elimina los portadores Unicode basados en edición (comprobable).
- La **Capa B** ataca las marcas de agua de muestreo mediante reescritura intensiva (best-effort; ataques estándar en la literatura como parafraseo / retrotraducción).
- Los **limpiadores de archivos** eliminan C2PA/XMP/props de los contenedores compatibles.
Hasta que los proveedores publiquen detectores y claves públicas, **ninguna herramienta puede certificar honestamente** "esto falla la comprobación oficial". Los informes deben separar el trabajo verificable del best-effort.
Prefiere un modelo **no originario** para la Capa B (no reescribas texto de Claude con Claude si intentas evitar re-marcarlo).
---
## Descargo de responsabilidad: lo que cuesta eliminar una marca de agua de texto
Las marcas de agua de texto viven en **la redacción misma**: la señal se distribuye entre las elecciones de tokens, por lo que casi cada frase lleva un poco de ella. De esto se derivan dos consecuencias, y son la razón por la que la Capa B se describe honestamente como *best-effort* en lugar de un borrador mágico.
1. **Eliminar significa reformular, no reestructurar.** Reordenar párrafos, cambiar encabezados o hacer retoques ligeros apenas mueven la señal. Eliminar una marca estadística requiere reescribir una fracción sustancial del texto — frase por frase, no sección por sección.
2. **Reformular degrada el texto.** Cualquier reescritura sustituye las elecciones de palabras originales por las del modelo que reescribe, lo que aplana el tono, la voz y la precisión. En textos de producción (SEO, marketing, trabajo para clientes) esa degradación es real y a menudo visible para quienes más se preocupan por la escritura. Es como tomar texto de un modelo de primer nivel y pedirle a un modelo menos capaz que lo reescriba desde cero: el resultado no puede superar el techo del modelo que reescribe.
Lo que lleva a la pregunta honesta de círculo completo:
> Si el plan es reescribir el texto con un modelo más barato de todos modos, ¿por qué pagar por un modelo premium en primer lugar? Generar directamente con el modelo más barato es más simple, más económico y produce el mismo — o mejor — resultado final.
La Capa B tiene sentido cuando quieres específicamente el **pensamiento y el borrador** del modelo premium y aceptas una pasada de reescritura para cumplir con un requisito de higiene o privacidad — no como una vía barata hacia texto libre de marcas.
**Cuándo omitir la Capa B:**
- **La calidad importa más que la higiene:** usa la vía sin pérdidas — la limpieza Unicode de la Capa A más los limpiadores de metadatos de archivos — y conserva la prosa original.
- **Reescribir de todos modos:** usa un modelo **no originario** (reescribir con el modelo de origen puede re-marcar el texto), y recuerda que el riesgo residual permanece — ninguna herramienta puede certificar que un detector de proveedor vaya a fallar.
---
## Formatos de archivo
| Formato | Inspeccionar | Limpiar |
| --- | --- | --- |
| PNG / JPEG / WebP | Chunks C2PA / APP11 / RIFF `C2PA`, indicios XMP de IA | Eliminar segmentos de metadatos |
| AVIF / HEIC | Cajas ISOBMFF `jumb` / XMP `uuid` | Eliminar cajas |
| BMP | Bytes finales no de imagen (sin canal estandarizado) | Truncar metadatos finales, corregir el campo de tamaño de archivo |
| GIF | Extensiones de aplicación Comment / XMP | Eliminar comment y XMP, conservar el bucle `NETSCAPE2.0` |
| TIFF (clásico + BigTIFF) | Etiquetas IFD: XMP, EXIF, GPS, IPTC, MakerNote | Eliminar etiquetas, poner a cero las cargas útiles, conservar las tiras |
| SVG | `<metadata>`, XMP | Eliminar bloques |
| PDF | Byte/XMP + herramientas opcionales | **exiftool** luego **qpdf**, luego **ghostscript** para metadatos dentro de imágenes incrustadas; cada herramienta ausente degrada una capa diferente (eliminación del documento, reescritura estructural, imágenes incrustadas) |
| DOCX | docProps / customXml | Limpiar props, eliminar customXml |
| EPUB | Metadatos OPF, meta/JSON-LD de XHTML, medios incrustados | Limpiar OPF, eliminar meta de XHTML, limpiar medios + Capa A (omite partes cifradas) |
| ODT | meta.xml | Eliminar metadatos de generador / con indicios de IA |
| HTML | meta, JSON-LD, data-ai* | Eliminar etiquetas/atributos |
| Markdown | Claves de IA en el frontmatter YAML | Eliminar claves + Capa A en el cuerpo |
| MP4 / MOV / M4A / M4V | Cajas ISOBMFF `jumb`/`uuid` (mismo mecanismo que AVIF/HEIC) + etiquetas de generador `moov/udta` | Eliminar cajas |
| WAV | Chunks RIFF `C2PA` / `LIST INFO`, chunk `id3\x20` incrustado | Eliminar chunks |
| MP3 | Tramas ID3v2 (v2.3/v2.4 por trama; v2.2 etiqueta completa) | Eliminar tramas coincidentes o la etiqueta completa |
| FLAC | Manifiesto C2PA en una trama ID3v2 `GEOB` | Eliminar la trama coincidente o la etiqueta ID3v2 completa |
El soporte de FLAC cubre el portador ID3v2 estandarizado de C2PA. Los bloques de metadatos nativos de FLAC,
los Vorbis Comments y las marcas de agua en el dominio de la forma de onda se dejan intactos.
#### Por qué PDF necesita qpdf, no solo exiftool
ExifTool escribe PDFs **de forma incremental**. `exiftool -all=` añade un
bloque `%BeginExifToolUpdate` que libera el objeto Info y elimina `/Info` del
trailer — pero los bytes de metadatos originales permanecen en el archivo textualmente, y
el propio exiftool puede deshacer la edición con `-PDF-update:all=`. El comando sale
con `0`, los visores no muestran metadatos, y el archivo se vuelve *más grande*, que es la señal reveladora.
Para una herramienta de eliminación de procedencia eso es una fuga silenciosa, así que `clean_pdf` continúa
la pasada de exiftool con `qpdf --linearize`, que reserializa el documento
a partir de su grafo de objetos y elimina los objetos ahora no referenciados. Sin `qpdf`
instalado la limpieza aún se ejecuta, pero lo indica:```
warning: exiftool PDF edits are incremental — the original metadata bytes
remain recoverable; install qpdf for a structural rewrite
Por qué qpdf no es suficiente para las imágenes dentro del PDF
Ambas pasadas anteriores trabajan sobre el documento: el diccionario Info, el paquete
XMP, el grafo de objetos. Ninguna desciende a un XObject de imagen, por lo que un escaneo o una
exportación de Photoshop —una página que es un gran JPEG— conserva lo que la imagen
lleva. En un PDF real exportado desde Photoshop eso deja 27 etiquetas en su sitio tras una
limpieza "exitosa", IFD0:Software, las marcas de tiempo de captura y una miniatura de vista previa entre ellas; un manifiesto C2PA adjunto a la misma imagen también sobrevive.
Así que clean_pdf añade una tercera pasada, deep_images, impulsada por el
pdfwrite de Ghostscript. Se ejecuta en dos niveles y se detiene en cuanto el archivo está limpio:
- Sin pérdida.
pdfwritecon pass-through reconstruye el documento desde el grafo de objetos mientras copia los datos de imagen comprimidos byte a byte —verificado hasheando los flujos antes y después. Esto elimina todo lo que el PDF envolvía alrededor de la imagen. El pass-through cubre los códecs que Ghostscript soporta para ello, JPEG (DCTDecode) y JPEG2000 (JPXDecode); las imágenes Flate, CCITT y LZW se decodifican y recodifican, lo cual es sin pérdida en la práctica para esos códecs pero no idéntico byte a byte.neveres la opción para un documento cuyos flujos deben sobrevivir intactos. - Recodificar, solo con evidencia. Todo lo que vive en los propios segmentos APPn del JPEG —EXIF en APP1, un manifiesto C2PA en APP11, recursos de Photoshop en
APP13— viaja con los bytes a los que está adjunto, por lo que el pass-through lo preserva. El nivel 2 ejecuta la misma pasada con el pass-through desactivado, y solo cuando el nivel 1 dejó algo atrás de forma demostrable: un marcador AI/C2PA en cualquier modo, o, bajo
always, cualquier metadato APPn superviviente. APP0 (JFIF) y APP2 (ICC) se dejan intactos —el primero es estructural y el segundo decide cómo se leen los colores. Los píxeles se gastan en evidencia, nunca en sospecha.
deep_images acepta auto (predeterminado: solo el nivel 1 cuando los marcadores sobrevivieron a la eliminación del documento, luego el nivel 2 si sobreviven a eso), always (nivel 1 para cada
PDF, escalando al nivel 2 también para EXIF de cámara y editor), lossless (solo nivel 1 —nunca recomprimir, e informar lo que sobreviva a través de los habituales
campos still_has_c2pa / post_findings) y never. Un valor no reconocido se
rechaza en lugar de tratarse silenciosamente como auto. El informe indica qué niveles se ejecutaron
mediante meta.deep_image_pass y meta.images_reencoded, y cuando la pasada se
omite nombra la opción que iría más lejos:```text
deep image pass not needed for AI/C2PA markers; pass deep_images="always"
to also clear non-AI EXIF inside images
Sin Ghostscript instalado, la limpieza aún se ejecuta e indica a qué no pudo
acceder:```text
warning: metadata inside embedded images left in place; install ghostscript
for the deep image pass
La eliminación de marcas de agua en el dominio de píxeles ahora está disponible como un backend externo opcional CtrlRegen (ver arriba); es un eliminador regenerativo, no una garantía. El soft binding de C2PA (marca de agua en el contenido que puede volver a vincular un manifiesto remoto de Content Credentials después de que se eliminen los metadatos) permanece fuera del alcance. Eliminar el C2PA de enlace fuerte no limpia esos canales.
Riesgo residual tras una limpieza
Esta herramienta reporta eliminaciones verificables (recuentos de Unicode, acciones sobre metadatos) y reescrituras de Capa B de mejor esfuerzo. No puede certificar que los detectores de los proveedores fallen.
Para comprobar tú mismo las señales residuales (opcional, externo):
| Canal | Qué eliminamos | Qué puede permanecer | Comprobación externa (ejemplos) |
|---|---|---|---|
| C2PA de enlace fuerte / EXIF / XMP | Sí | Marcas de enlace débil / de píxeles | c2patool, Content Credentials verify |
| Medios de clase SynthID | Eliminación opcional de píxeles (CtrlRegen externo); puntuación local en caso contrario | Marca de agua de audio/vídeo; marca de agua residual de píxeles tras la eliminación | Herramientas del proveedor (p. ej. Google SynthID / detector de Vertex cuando se ofrezca); puntuador local opcional reverse-SynthID |
| Texto estadístico | Reescritura de mejor esfuerzo | Marcas fuertes tras una edición ligera | No hay detector universal público; herramientas del proveedor cuando estén disponibles |
Contexto industrial de dos capas (C2PA + marca de agua imperceptible): guía del Institute of AI PM.
Detectores de marcas de agua
Comprobadores proporcionados por proveedores para verificar si el contenido lleva marcas de procedencia de IA:
- Claude: Check if a file was made with Claude — lee las credenciales de contenido C2PA en imágenes, vídeo y audio para informar si Claude participó en la producción del archivo; se ejecuta en el navegador. La API de detección de marcas de agua en texto de Claude está actualmente en vista previa privada.
- OpenAI: Verify OpenAI-generated content — sube una imagen o un archivo de audio y comprueba si hay señales de procedencia de OpenAI (metadatos C2PA y marcas de agua SynthID). También hay disponible una API programática.
- Google DeepMind: SynthID — la tecnología de marcado de agua de Google para imágenes, audio, texto y vídeo generados por IA, con una descripción general de cómo se incrustan y detectan las marcas imperceptibles.
- Gemini: Verify AI-generated images, videos, and audio — la guía de Google para verificar archivos en la aplicación Gemini usando marcas de agua SynthID y Content Credentials, incluidos los límites de subida y cómo interpretar los resultados.
Opciones de eliminación (resumen)
| Opción | Elimina | Notas |
|---|---|---|
| Limpieza de Unicode (Capa A) | ZWSP, bidi, etiquetas, espacios exóticos, … | Opción segura por defecto para texto |
| Reescritura (Capa B) | Marcas estadísticas de tokens (mejor esfuerzo) | Siempre ofrecida por la skill; cuesta estilo — ver Disclaimer |
| Eliminación de contenedor/metadatos | Procedencia del archivo | Ver tabla de formatos |
| Eliminación de píxeles CtrlRegen (opcional) | Marcas de imagen en el dominio de píxeles (clase SynthID, StegaStamp, Tree-Ring, StableSignature) | Backend externo; cómputo intensivo; intensidad conservadora por defecto |
| Eliminación de píxeles DiffusionPurification (opcional) | Marcas de imagen en el dominio de píxeles (clase Tree-Ring) | Backend MarkDiffusion; regeneración ciega (más deriva que CtrlRegen); intensidad conservadora por defecto |
| Modelos locales de pesos abiertos | Evitar volver a estampar con el modelo de origen | Alternativa operativa |
Matriz: skills/remove-ai-marks/references/removal-matrix.md.
Ética y descargo de responsabilidad
Ver skills/remove-ai-marks/references/ethics.md. Para privacidad e investigación sobre tu contenido — no para fraude académico ni afirmaciones falsas de “escrito por humanos”.
Uso responsable: Este proyecto es para contenido que posees o estás autorizado a procesar. Los usuarios deben cumplir la normativa local y usarlo de forma responsable. Los desarrolladores declinan cualquier responsabilidad por el posible uso indebido por parte de los usuarios.
Ecosistema
Proyectos de terceros que envuelven o complementan este repositorio, listados solo para facilitar su descubrimiento. No están mantenidos, respaldados ni soportados por este proyecto. Este proyecto no revisa su código, no avala su comportamiento ni sus garantías, ni asume responsabilidad por nada que instales o ejecutes desde esta lista. Cada proyecto se rige por su propia licencia, mantenedores y documentación — léelos antes de usarlo.
MetaClean — GUI de escritorio
MetaClean es una aplicación de escritorio independiente en Rust/Tauri con licencia MIT (Windows, macOS, Linux) que ofrece una GUI nativa empaquetada para la limpieza de metadatos mediante arrastrar y soltar, con bandeja del sistema e integración con el Explorador. Es una base de código separada: no llama al servicio Python de este repositorio, y sus formatos soportados y garantías de limpieza difieren de las de este proyecto. Ver su README para más detalles.
unmark-web — interfaz web de navegador
unmark-web es un cliente web estático independiente con licencia MIT. Elimina marcas Unicode invisibles del texto y quita metadatos de procedencia de las imágenes enteramente en el navegador, y opcionalmente puede llamar al servicio HTTP de este repositorio para los formatos que no maneja localmente. Es una base de código separada y no está afiliada a este proyecto; ver su README para el alcance y los límites.
DropMarks — GUI de macOS
DropMarks es una aplicación independiente de macOS en SwiftUI con licencia MIT. Llama a inspect_file.py / clean_file.py de este repositorio (y opcionalmente a rewrite_text.py) mediante una copia vendorizada de esos scripts de la stdlib. Es una base de código separada y no está afiliada a este proyecto; ver su README para el alcance y los límites.
Añadir un proyecto
Para registrar un proyecto aquí, abre un PR añadiendo una entrada breve — nombre del proyecto, qué envuelve o añade, y un enlace a su propio repositorio. Mantén las entradas breves y factuales; no afirmes compatibilidad con este proyecto ni respaldo por su parte. Un proyecto listado debería construirse sobre este repositorio o integrarlo — por ejemplo, llamando a su servicio o reutilizando su motor de detección — en lugar de limitarse a abordar el mismo problema de forma independiente. Por favor, evita nombres que empiecen por watermarks-remover o se parezcan mucho — los nombres similares dificultan distinguir qué proyecto es cuál.
Hook de pre-commit
El control de CI ya existe (exportación SARIF de audit_dir.py, ver el contexto de la matriz de cobertura) — los hooks de pre-commit de abajo detectan la misma clase de problema antes, incluso antes de que se confirme un archivo marcado. Ambos envuelven las CLI existentes (audit_dir.py / clean_file.py) — sin lógica de detección separada.```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` falla el commit y lista los hallazgos; `watermarks-remover-clean` es opcional y reescribe los archivos preparados en el lugar (sale con 1 para que revises el diff y vuelvas a prepararlos — la misma convención que los hooks de corrección automática como `ruff --fix`). Cuando el limpiador no puede procesar un archivo en absoluto — se bloqueó, fue terminado o no produjo ningún informe — `watermarks-remover-clean` nombra ese archivo y sale con 3 en su lugar, de modo que un limpiador que falló nunca se confunda con un archivo que ya estaba limpio. Ejecuta cualquiera de los dos manualmente con `python3 service/scripts/check_staged.py <files...>` / `clean_staged.py <files...>`.
## Tests```bash
python3 -m venv .venv && .venv/bin/pip install pytest
.venv/bin/python -m pytest # or: make test
make smoke # quick CLI smoke on fixtures
Registro de cambios
v0.7.0 — reescritura de la Capa B en /clean, módulo de robo de marcas de agua, eliminación de marcas de agua en audio/vídeo y mayor amplitud de benchmarks/herramientas
v0.7.0 incorpora la reescritura de marcas estadísticas de la Capa B dentro del propio servicio /clean, impulsada por una estrategia configurable y ajustada mediante benchmarks ([email protected],[email protected]). Junto a ello: un módulo de caja negra para robar marcas de agua, eliminación destructiva de marcas de agua en audio y por fotograma en vídeo, un benchmark de reescritura sustancialmente más rico y un conjunto de correcciones de endurecimiento, seguridad y herramientas.
Reescritura de la Capa B en el servicio
/cleanejecuta la reescritura de la Capa B para texto después de la Capa A. El valor predeterminado proviene deconfig/clean_strategy.json; unoptions.strategypor solicitud lo anula, y/cleanrechaza con 400 cuando el backend requerido no está configurado (#315). Precedencia de configuración:--strategy-config>WATERMARKS_CLEAN_STRATEGY_FILE>config/clean_strategy.json.- Nueva táctica de reescritura
mlm: enmascara una fracción de palabras de contenido y rellena conroberta-large— una edición local no autorregresiva, por lo que la salida mezcla el flujo de tokens original con predicciones del LM enmascarado (#311). - La táctica
humanizeahora aplica la pasada de habilidad humanizadora de forma determinista (comillas rectas, sin guiones em/en, colapsos de relleno,utilize→use) y nombra las reglas de escritura humana en el prompt (#311).rewrite_text.pyincorporó una ruta CLI--strategy. - Corrección de la reescritura: tokenización de palabras Unicode en la divergencia léxica (#305); comparar márgenes sin procesar antes del redondeo y registrar metadatos de selección / valores p clasificados (#249).
Benchmark
- Búsqueda de recetas SynthID + medición robusta (#280); vocabulario de reescritura renombrado, búsqueda entre entradas y ordenación humanize-last (#302); recomendar solo estrategias que aún superen tras el pulido de humanización (#307).
- API masiva de Pangram como backend de semejanza humana (#296); benchmark endurecido de nivel de reescritura mínima con un corpus de 30 documentos (#257); cuadrícula de pesos validada + búsqueda de recetas ampliada (#294); corpus de benchmark en polaco (#295).
Robo de marcas de agua
- Nuevo módulo de caja negra para robar marcas de agua y descargador de corpus de prompts (#303); limpiar el estado obsoleto ante un fallo de sonda de reinicio (#310).
Audio / vídeo / imagen
- Cadena destructiva de eliminación de marcas de agua en audio para silentcipher/AudioSeal/WavMark (tempo + tono + EQ + recodificación a baja tasa de bits → M4A) (#266).
- Purificación de vídeo TrustMark por fotograma que colapsa el voto temporal (#265).
- Caja
uuidde procedencia de contenido C2PA reconocida en MP4/MOV/AVIF/HEIC (#264). - Preservar las colas truncadas de MP4 durante el stripping (#242); mantener el destino de recodificación de audio distinto del destino de limpieza del contenedor (#278).
- Omitir la salida descartada de exiftool y el SynthID redundante en el escaneo posterior a la limpieza (#261); degradar limpiamente cuando exiftool no puede procesar un PDF (#281).
- Limitar el
zTXt/iTXtdescomprimido de PNG a 1 MiB (#308); eliminar las declaraciones DOCTYPE/ENTITY de XML en SVG (#288); mantener los miembros binarios de DOCX seguros a nivel de bytes (#314); preservarAppVersionde OOXML (#289).
Servicio HTTP y CLI
- Opción de
/cleanpara conservar espacios exóticos, en consonancia con la CLI (#274);/inspectexpone clases de evidencia explícitas en la carga útil sospechosa (#277); marcas de tiempo en los registros de solicitudes HTTP (#256); canalizar los bytes de la carga útil hacia la puntuación SynthID de HTTP yinspect_*para evitar una lectura redundante. clean_file.pyincorporó-q/--quiet/--only-changed(#254).
Habilidades, plugin y hooks
- Puntuación estilométrica y palancas de detección para
clean-user-facing-text(#258); lanzador del hook PostToolUse hecho multiplataforma (#255); el hook pre-commit trata los archivos no de texto limpios idénticos a nivel de bytes como modificados (#238).
Auditoría
audit_dir.pyescanea archivos de código fuente, documentación e i18n que el enrutador pasó por alto (#284); escanea.ts/.tsx/.jsx/.gdy alinea la confianza de espacios entre formatos (#273); soporte deaudit_website.py --sarif(#194); endurecer las copias de seguridad en el lugar, el estado de archivos limpios, el veredicto de SynthID, ID3v2 truncado y el enrutamiento de zip (#201).
Seguridad
- Eliminar ReDoS polinómico en los escaneos de data-URI y JSON-LD (#306); bloquear las redirecciones HTTP en el puntuador SynthID para prevenir SSRF (#252).
CI, herramientas y documentación
- La CI falla cuando los requisitos de backends opcionales no pueden resolverse (#301); la imagen Docker informa ffmpeg como utilizable e instala Ghostscript (#272); actualizaciones de dependencias (cython #299, scipy #298, ruff #297, docker/setup-buildx-action #237).
- Documentación: sección Watermark Detectors, referencia al blog "Probing SynthID" de ETH SRI, política del ecosistema (eliminar ClaudeWatermarks; exigir que los proyectos listados usen este repositorio) (#292).
v0.6.0 — mayor cobertura de formatos, endurecimiento de la Capa A, distribución de plugin y hooks, y reescritura guiada por detección
Cobertura de formatos y contenedores
- AVIF / HEIC: eliminación nativa de metadatos y C2PA con la biblioteca estándar (#84, #85)
- BMP / GIF / TIFF: detección, inspección y limpieza de metadatos con la biblioteca estándar — las extensiones de comentario/XMP de GIF se descartan mientras que el bucle
NETSCAPE2.0y otros fragmentos de animación se preservan; los metadatos IFD de TIFF (XMP/EXIF/GPS/IPTC/MakerNote) se descartan con las cargas útiles puestas a cero y los desplazamientos de strip conservados, tanto para TIFF clásico como BigTIFF; los metadatos finales de BMP se truncan con el campo de tamaño de archivo reescrito (#107) - EPUB: limpieza de contenedores con la biblioteca estándar — metadatos OPF y meta/JSON-LD de XHTML depurados, medios raster/SVG incrustados eliminados, Capa A aplicada al texto del cuerpo XHTML, partes de metadatos portadoras de marcadores descartadas y partes cifradas OCF pasadas sin modificar (#107)
- XLSX / PPTX / DOCX (OOXML): depuración nativa con la biblioteca estándar de metadatos de contenedor, texto y medios incrustados; vaciar siempre los campos de procedencia
docPropsde DOCX; podar relaciones colgantes tras la eliminación decustomXml; ejecutar la Capa A sobre el texto del cuerpo de DOCX/ODT; decodificar entidades XML antes de la depuración de la Capa A (#91, #100, #76, #83, #73, #80, #74, #81, #142) - Contenedores SGML/vectoriales: eliminación de metadatos en tiempo lineal para SVG/ODT (GHSA-7vpp-96qp-j9wh) (#147); inspeccionar y limpiar recursivamente los data URIs de raster incrustados en SVGs, HTML y Markdown (#87, #88)
- Audio / vídeo: eliminación de metadatos AI/C2PA para MP4/MOV, WAV y MP3 (#139); detección y eliminación del fragmento C2PA de WAV RIFF; soporte de metadatos C2PA de FLAC; rechazar el análisis parcial de fotogramas ID3v2 (#232); preservar los desplazamientos de medios de MP4 al eliminar metadatos (#183)
- PDF: alcanzar metadatos que residen dentro de imágenes incrustadas y dejar de redimensionar el PDF para eliminar XMP; ejecutar la pasada de imagen profunda tanto si exiftool está instalado como si no; respetar los bytes de relleno de marcadores JPEG y compartir un único recorredor de segmentos
- PNG: detectar nombres de productos de generadores de IA en los metadatos de texto de PNG; detectar marcadores de IA en texto PNG comprimido (#127); conservar la cola truncada en lugar de descartarla en los strips de png/isobmff (#182)
Endurecimiento de la Capa A (Unicode invisible)
- Endurecimiento consolidado de la Capa A (#133): eliminar puntos de código
Default_Ignorablereservados sin un uso legítimo de intercambio (U+2065,U+FFF0–U+FFF8,U+E0000,U+E0080–U+E00FF,U+E01F0–U+E0FFF— reportados comoreserved_ignorable), los 66 no-caracteres (U+FDD0–U+FDEFmásU+FFFE/U+FFFFpor plano — reportados comononcharacter), y tres portadores Default_Ignorable de renderizado en blanco que el comodínCfnunca vio (U+180F,U+3164,U+FFA0). Cada uno tiene la misma preservación en contexto que sus hermanos ya cubiertos, por lo que el texto de sílabas parciales no se corrompe, y cada uno se aplica tanto al motor del servicio como a la copia ligera de la habilidad incluida - Dejar de eliminar controles de formato de diseño visible junto a su propio script: los controles de cuadrat de jeroglíficos egipcios (
U+13430–U+1343F), los controles de taquigrafía Duployan (U+1BCA0–U+1BCA3) y los controles musicales de beam/tie/slur/phrase (U+1D173–U+1D17A) ahora se preservan cuando son adyacentes a su propio script y aún se eliminan (y se marcan) cuando flotan entre texto no relacionado; el modo paranoico--strip-emoji-glueaún los elimina en todas partes - Pulido de emoji / script: preservar VS16 después de emojis individuales fuera de los rangos de bloque; preservar joiners de script, emoji de bandera y marcas Cf árabes; preservar Unicode multilingüe durante la limpieza de texto (#34)
Reescritura de la Capa B y detección de marcas de agua
- Reescritura iterativa de la Capa B guiada por detección: cada ronda genera variantes
--candidates(predeterminado 1,WATERMARKS_REWRITE_CANDIDATES) y--max-loops(predeterminado 1,WATERMARKS_REWRITE_LOOPS) limita las rondas de evaluación, deteniéndose en cuanto un intento supera la detección. Prioridad del evaluador: MarkLLM (--markllm-scheme) > divergencia léxica bigram-Jaccard (reserva).rewrite_text.py --json-statsahora informaevaluator/max_loops/attempts_made/passedycandidate_scorespor intento (#153) - Verificación de misma clave Keyed-Gumbel (Aaronson EXP): el nuevo
detect_gumbel.py, solo con la biblioteca estándar, implementa la prueba de reproducción sin modelo (u = PRF(Hash(key, window), token); valor p exacto de cola Gamma; enmascaramiento de ventana repetida) sin GPU, modelo ni logits.rewrite_text.py --gumbel-key(variable de entornoWATERMARKS_GUMBEL_KEY, preferida) lo convierte en el evaluador del bucle iterativo (prioridad: gumbel > markllm > divergencia léxica) y se expone comogumbelen/capabilitiesy/detect. Solo con la misma clave — no es un oráculo de proveedor; la clave nunca se registra (#190) - Benchmarks: benchmark de texto MarkLLM multi-esquema y detección (#188) y un benchmark reproducible de eliminación de texto SynthID (#145); variantes predeterminadas
paraphrase:3; el informe y el CSV llevan los intentos por documento (columnasmean_attempts/att,attempts/evaluator/passed);--rewrite-loopsrefleja--max-loops - Detección: detección de marcas de agua de texto de proveedores (Gemini SynthID, Claude seam, MarkLLM) más un sidecar puntuador de imágenes SynthID (#109); nuevo detector de texto de IA estadístico y estilométrico sin LLM para CI y auditorías (#68, #69)
Distribución: instalaciones de plugin, hooks y habilidad
- El repositorio es ahora un plugin de Claude Code y un marketplace de un solo plugin (
.claude-plugin/plugin.json+marketplace.json), por lo que ambas habilidades se instalan con/plugin marketplace add guillaumemeyer/watermarks-removery luego/plugin install watermarks-remover@watermarks-remover, y se actualizan en el lugar.make plugin-validateejecutaclaude plugin validate . --strict;tests/test_plugin_manifest.pyverifica los manifiestos sin la CLI install_skill.pyincorporó un--target(claude-code,claude-project,cowork,cursor) y un selector--skillque cubre ambas habilidades incluidas, además de--list,--linkyCLAUDE_CONFIG_DIR. El destinocoworkconstruye un paquete de carga reproducible (dist/<skill>.zip, directorio de habilidad único de nivel superior); cada destino se valida contra las reglas de empaquetado de Agent Skills y el límite de carga de 30 MB. Nuevos destinosmake:install-claude-code-skill,install-claude-code-text-skill,install-claude-project-skill,package-cowork-skill,package-cowork-text-skill- Autolimpieza determinista mediante un hook
PostToolUse(hooks/hooks.json+service/scripts/hook_written_file.py): después de que el agente escribe un archivo, el arnés ejecuta el hook tanto si el modelo coopera como si no.check(predeterminado) informa las marcas al modelo;cleanlas elimina en el lugar y le dice al modelo que el archivo se movió, intercambiando solo ante una diferencia real para que los archivos limpios conserven su mtime. El modo proviene de la configuraciónhook_modedel plugin o deWATERMARKS_HOOK_MODE; la detección reutilizaaudit_lib.scan_file/is_actionable, por lo que el hook, la puerta pre-commit y la exportación SARIF de CI coinciden. Un hook aún no puede reescribir el mensaje de chat del asistente — no existe tal punto de hook — por lo que esa ruta sigue siendo de mejor esfuerzo - Integración del hook pre-commit para la comprobación/limpieza de archivos preparados (#138); habilidad de texto ligera para Cursor (#35); la descripción de
clean-user-facing-textya no nombra a Cursor como el único anfitrión
Servicio HTTP
- Endpoints por lotes:
POST /clean/batch,/inspect/batch(#137) yPOST /detect/batch(#151) - Preservar las extensiones de formato de imagen en
/cleany usar escrituras seguras enav_meta(#150); usar base64 portátil en el ejemplo curl de/detect(y corregir la portabilidad derealpathen macOS en los bootstraps, #185)
Auditoría / inspección y seguridad
audit_dir.pyincorporó concurrencia multi-worker y exportación SARIF 2.1.0 (#101, #102)- Enrutar los formatos binarios del sitio web a sus escáneres reales (#177); rechazar bombas DTD/entidad en el analizador de sitemap (GHSA-pjg6-92pm-mmcf) (#146); un limpiador que falla bloquea el commit en lugar de leerse como limpio (#179); un archivo de texto ilegible es un escaneo fallido, no uno limpio (#169)
Correcciones de fiabilidad y exactitud
- Una segunda ejecución de
--in-placepreserva el.bakoriginal; conservar la evidencia recopilada cuando un miembro posterior del zip no se puede leer (#175); los contenedores ISOBMFF truncados aún ejecutan el respaldo de escaneo de bytes C2PA (#176); distinguir un limpiador fallido de un archivo ya limpio (#159, #161); tratar una ejecución fallida de c2patool como no concluyente en lugar de "sin C2PA" (#156); validar los tipos de opciones de limpieza (#111); nunca seleccionar automáticamente el dispositivo MPS para la detección de marcas de agua de texto (#99); portabilidad en macOS — stdout puro--jsonpara el puntuador SynthID y sondarealpathde BSD (#70); corregir una ruta desubprocess_creationflagsde Windows en_ghostscript_usabley evitar que los procesos hijos abran una ventana de consola en Windows - Endurecimiento del comportamiento: preservar el modo keep de comentarios JPEG benignos; corregir el flag tragado de
bench-synthid-text; simplificar el paso de flags para la sonda de Ghostscript y el noqa innecesario de clean_text (lint)
CI / herramientas / documentación
- Linting y formateo con Ruff con aplicación en CI (#103); añadir macOS a la matriz de pruebas (#152); añadir una configuración de CodeRabbit para revisiones automatizadas de PR (#222); CODEOWNERS para CODE_OF_CONDUCT/LICENSE y propietarios de revisión principal; atribuir los derechos de autor a Guillaume Meyer y colaboradores (#228)
- Documentación: guía de reescritura que preserva la voz y protección de las opciones de voz/accesibilidad; adiciones al ecosistema (ClaudeWatermarks, unmark-web) y una nota que desaconseja nombres similares; referencia arXiv 2402.14904; guía de inicio automático en Windows mediante Task Scheduler; base64 portátil en los ejemplos curl; fijar el motor de texto de la habilidad Cursor incluida a la copia del servicio (#96)
Sin publicar
- Hook de limpieza pre-commit (
watermarks-remover-clean/clean_staged.py): usar resúmenes de contenido (SHA-256) y detección de acciones activas para que los archivos limpios en disco se reconozcan sin exigir un re-staging infinito (#173) - Preservación de contenedores OOXML: mantener
<AppVersion>intacto endocProps/app.xmldurante la limpieza de metadatos de DOCX, XLSX y PPTX para satisfacer las restricciones del esquema ECMA-376 y evitar errores de "contenido ilegible" de Microsoft Word/Office (#283)
v0.5.0 — distribución de servicio y Docker, API HTTP y arneses de verificación
Distribución de servicio / Docker
- División habilidad/servicio: la habilidad (
skills/remove-ai-marks/) es ahora un cliente remoto sin código sobre HTTP; toda la implementación se trasladó aservice/scripts/y se ejecuta detrás deserver.py, un punto de entrada HTTP con la biblioteca estándar (/health,/inspect,/clean,/capabilities) - Servicio HTTP:
service/scripts/server.pyexpone el pipeline de limpieza sobre JSON/base64; el endurecimiento refleja las CLIs (límites de tamaño, guarda binaria, escrituras atómicas, loopback predeterminado, autenticación bearer opcional conWATERMARKS_SERVER_API_KEY) - OpenAPI:
GET /openapi.jsonsirve una especificación OpenAPI 3.0.3 generada dinámicamente (construida a partir de la tabla de rutas + configuración en vivo, por lo que nunca se desvía de los endpoints reales); la CI la valida conopenapi-spec-validator - Imagen Docker principal (
service/Dockerfile): servicio de limpieza completo con exiftool / qpdf / c2patool preinstalados; cualquier CLI sigue siendo ejecutable anulando el comando - Docker / compose:
compose.yamllevanta toda la infraestructura (coresiempre;markllm/markdiffusiondetrás deprofile: harness;ctrlregen/synthiddetrás deprofile: heavycomo compilaciones solo locales); los servicios llevan el prefijowr-; los servicios harness/heavy usan por defectocommand: ["--help"]para quedocker compose up --profile harness --profile heavysalga limpiamente (las CLIs de un solo uso se ejecutan condocker compose run); los nuevosmake compose-check/compose-check.shvalidan la pila en ejecución (solo código de salida) - Publicación en GHCR:
.github/workflows/release-images.ymlpublica las imágenescore,markllm,markdiffusionen etiquetasv*;ctrlregen/synthidnunca se publican (licencias upstream) - Configuración por variables de entorno:
.env.example+ guía de configuración del servicio;docker composecarga.envautomáticamente;.envestá en gitignore (denegar por defecto) - Higiene del repositorio:
.gitignoreyservice/.dockerignoreahora deniegan por defecto — solo las rutas explícitamente permitidas pueden confirmarse o enviarse en un contexto de compilación (los contextos de imagen solo envíanservice/scripts/, que es todo lo que copian los Dockerfiles) - Pruebas:
tests/test_http_server.py(13 casos) para el servicio HTTP; todas las suites redirigidas aservice/scripts/
Arnés de marcas de agua de imagen MarkDiffusion (opcional)
- Nuevo arnés opcional (externo
THU-BPM/MarkDiffusion, Apache-2.0):markdiffusion_harness.pycon subcomandoswatermark/detect/purifypara nueve esquemas de imagen (Tree-Ring, Ring-ID, ROBIN, WIND, SFW, Gaussian-Shading, GaussMarker, PRC, SEAL) clean_image.py --remove-pixel diffusionejecuta el ataque de regeneraciónDiffusionPurificationde MarkDiffusion como motor alternativo de eliminación de píxeles (intensidad conservadora 0.3 por defecto)- Bootstrap
setup_markdiffusion.sh(pin de PyPI1.0.2; clon editable con--checkouten un commit fijado) +requirements-markdiffusion.txt+Dockerfile.markdiffusiony Makefilebootstrap-markdiffusion/smoke-markdiffusion/docker-markdiffusion-build/docker-markdiffusion-help - Pruebas basadas en mocks (
tests/test_markdiffusion_harness.py) — sin torch en CI; documento de referenciareferences/markdiffusion.md - Documentación: advertencia de verificación solo con el mismo esquema (no es un oráculo de detector de proveedor) y advertencia de deriva por regeneración ciega en README, SKILL.md,
removal-matrix.md,markdiffusion.md
Arnés de marcas de agua de texto MarkLLM (opcional)
- Nuevo arnés opcional (checkout externo
THU-BPM/MarkLLM, Apache-2.0):detect_text_watermark.pycon subcomandosdetect/watermarkpara los esquemas KGW y SynthID rewrite_text.py --markllm-schemeejecuta la detección antes/después alrededor de una reescritura de la Capa B y la detección por candidato cuando--candidates N>1(controlado por variable de entorno; informacleared)- Bootstrap
setup_markllm.sh+requirements-markllm.txt(dependencias fijadas) +Dockerfile.markllmy Makefilebootstrap-markllm/smoke-markllm/docker-markllm-build/docker-markllm-help - Endurecimiento: carga de modelos solo desde caché con
--offline(sin salida a HF, sin código remoto), límite de configuración de 1 MiB,WATERMARKS_MARKLLM_RLIMIT_ASopcional en el subproceso de reescritura, torch fijado en el Dockerfile y verificación del SHA del clon enDockerfile.markllm - Pruebas basadas en mocks (
tests/test_markllm_detect.py, 21 casos) — sin torch en CI; advertencia del arnés de verificación (solo con la misma configuración, no es un oráculo de detector de proveedor) documentada en README, SKILL.md,removal-matrix.md,vendor-notes.md
Correcciones y pulido- Capa B: rewrite_text.py ahora envía reasoning_effort: "none" por defecto para backends openai-compatible (--reasoning-effort / WATERMARKS_REWRITE_REASONING_EFFORT; off lo omite). Los modelos de razonamiento como deepseek-v4-flash de lo contrario consumen ~100s de cadena de pensamiento en una reescritura de una línea (9.894 vs 12 tokens de finalización)
- Corregir la compilación de la imagen markllm:
requirements-markllm.txtfijabatokenizers==0.23.1, que entra en conflicto contransformers==5.15.0(limita atokenizers<=0.23.0; no existe una versión 0.23.0) — ahora fijado atokenizers==0.22.2; torch se movió al índice de ruedas de CPU (torch==2.13.0.*) para que la imagen sea solo de CPU comoDockerfile.markdiffusion - Corregir la compilación de la imagen ctrlregen: las fijaciones de investigación de la era de 2023 (
safetensors==0.4.3,transformers==4.37.2→tokenizers<0.19) no incluyen ruedas para Python 3.14, por lo que la imagen base ahora espython:3.11-slim(fijada por digest, multi-arquitectura) - Corregir las imágenes del arnés en tiempo de ejecución:
Dockerfile.markllmyDockerfile.markdiffusionnunca copiabancommon.pya/app(error preexistente) — añadido - WebP: inspección y limpieza de metadatos solo con la biblioteca estándar para los fragmentos RIFF
C2PA, XMP, EXIF e ICC (#37) - BMP / GIF / TIFF: detección, inspección y limpieza de metadatos solo con la biblioteca estándar — las extensiones de comentario/XMP de GIF se descartan mientras que el bucle
NETSCAPE2.0se preserva; los metadatos IFD de TIFF (XMP/EXIF/GPS/IPTC/MakerNote) se descartan con las cargas útiles puestas a cero y los desplazamientos de tira conservados, tanto para TIFF clásico como BigTIFF; los metadatos finales de BMP se truncan con el campo de tamaño de archivo reescrito - EPUB: limpieza de contenedor solo con la biblioteca estándar — metadatos OPF y meta/JSON-LD de XHTML depurados, medios raster/SVG incrustados eliminados, Capa A aplicada al texto del cuerpo XHTML, partes de metadatos portadoras de marcadores descartadas y partes cifradas con OCF pasadas sin modificar
- Saneamiento de nombres de archivo: el servicio HTTP rechaza nombres de salida no seguros proporcionados por el cliente
- Corregir el limpiador de frontmatter de markdown que fallaba y filtraba claves de IA anidadas (#25)
- Las herramientas de texto rechazan la entrada binaria;
--force-textlo anula (#24) --jsonya no suprime el código de salida de señal residual (#30)inspect_fileimprime el nombre del archivo en su salida (#50)- Preservar las metaetiquetas de generador CMS con mayúsculas y minúsculas mixtas (#42)
- Preservar los invisibles de script portantes, eliminar PUA en la Capa A (#38, #52)
- Preservar los joiners de script, emoji de bandera y marcas Cf árabes en la Capa A (#28)
- Reforzar la auditoría del sitio web contra SSRF y bombas gzip (#49)
- SECURITY.md solo hace referencia al canal de avisos privados (#51)
- Windows: ports de PowerShell de los bootstraps de configuración (#40)
- Documentación: añadir escudos de estrellas/forks y eliminar el gráfico de historial de estrellas; añadir MarkLLM a las referencias del README; plantilla de pull request; plan para el despliegue de Docker CLI + API
v0.4.0 — eliminación de píxeles, confianza en los hallazgos, correcciones de Windows y falsos positivos
Eliminación opcional de píxeles CtrlRegen (backend externo)
- Eliminación opcional de marcas de agua en el dominio de píxeles mediante un checkout externo de
mertizci/noai-watermark: adaptadorclean_ctrlregen.py+ bootstrapsetup_ctrlregen.sh(commit fijado, checkout disperso, venv, verificación SHA), másDockerfile.ctrlregenymake bootstrap-ctrlregen/docker-ctrlregen-build/smoke-ctrlregen clean_image.py --remove-pixel ctrlregenejecuta eliminación de metadatos → eliminación de CtrlRegen → puntuación opcional de reverse-SynthID antes/después;inspect_image.pysugiere el flag ante una puntuación SynthID alta- Intensidad predeterminada conservadora
0.25(presets 0.15/0.25/0.35/0.5/0.7); la tubería nativa de 512×512 es auto-tiled por el backend para imágenes más grandes; el subproceso de torch obtiene límites de recursos más altos anulables por variables de entorno - El backend nunca se incluye:
noai-watermarkno incluye archivo LICENSE (tratado como todos los derechos reservados), y sus rutas de código de autoinstalación/reinicio se evitan usandoCtrlRegenEnginedirectamente
Confianza en los hallazgos y auditorías agregadas
- Los hallazgos ahora se clasifican como
confirmed/probable/informational/likely_false_positive, expuestos en JSON de texto/imagen/contenedor e informes humanos - Nuevos
audit_dir.py(árbol recursivo) yaudit_website.py(descubrimiento de sitemap + rastreo) que agregan informes; documentados en SKILL.md
Correcciones de falsos positivos
- DOCX: escanear solo
docProps/customXml, no el cuerpo visible (#14) - Capa A de texto: preservar emoji
VS16/ZWJdespués de una base de emoji; nuevo flag paranoico--strip-emoji-glue(#22) - HTML: tratar las etiquetas de generador CMS como informativas, no como metadatos de IA (#13)
- PDF: excluir las cargas útiles de flujo del escaneo de bytes de marcadores de IA (#13)
- Los informes de inspección anotan rutas no compatibles/de mejor esfuerzo
Soporte de Windows
- Condicionar
preexec_fnyos.fchmodsolo para POSIX para que las escrituras y herramientas opcionales se ejecuten en Windows (#15, #23) - Reconfigurar stdio a UTF-8 para que los flujos redirigidos de Windows ya no fallen con Unicode invisible; etapa de CI de Windows + ejecución de smoke de CLI (#23)
Documentación y cadena de suministro
- Sección CtrlRegen del README + referencias de investigación (CtrlRegen, UnMarker, advertencia de sigilo forense), descargo de responsabilidad de uso responsable; actualizaciones de SKILL/matrix/vendor-notes/ethics
- Configuración de Dependabot + CODEOWNERS de rutas de seguridad; actualizar scipy/numpy/opencv-python/scikit-learn/pywavelets y la imagen base a Python 3.14-slim
- Pruebas de CtrlRegen basadas en mocks (sin torch en CI)
v0.3.2 — refuerzo de seguridad (escrituras seguras, cliente HTTP, cadena de suministro de CI)
- Escrituras de salida seguras y atómicas: cada limpiador ahora escribe mediante archivo temporal + renombrado atómico (
safe_write_bytes/safe_write_text), rechaza destinos con enlaces simbólicos y crea copias de seguridad.baka través de la misma ruta segura — los enlaces simbólicos precolocados (p. ej., en/tmpo directorios de descarga) ya no pueden redirigir una escritura limpia a un archivo arbitrario - Refuerzo del cliente HTTP de
rewrite_text.py: las redirecciones se rechazan de plano, por lo que una clave de API en el encabezadoAuthorizationnunca puede reenviarse a un host no validado; los endpoints que no son loopback están denegados por defecto (optar con--allow-remoteoWATERMARKS_REWRITE_ALLOW_REMOTE=1); solo se aceptan esquemas http(s); se eliminó--api-key— las claves son solo por variables de entorno medianteWATERMARKS_REWRITE_API_KEY - Límites de recursos: entrada máxima predeterminada 1 GiB → 256 MiB, nuevo límite de stdin de 64 MiB, presupuesto de zip DOCX/ODT 512 MiB → 128 MiB, y
RLIMIT_AS/RLIMIT_FSIZEaplicados a los subprocesos de exiftool/c2patool/SynthID (todos los límites anulables por variables de entorno) - Cadena de suministro: acciones de CI fijadas por SHA con
permissions: contents: read, dependencias de desarrollo fijadas (requirements-dev.txt), un paso depip-audity un nuevo flujo de trabajo de CodeQL; la imagen de Docker ahora se ejecuta como usuario sin privilegios con pip fijado - Dependencias del scorer: Pillow actualizado de 10.4.0 → 12.3.0 (24 CVE conocidos); uso de la API verificado contra el commit upstream fijado
- Pruebas: 18 nuevas pruebas de regresión de seguridad (60 en total, todas pasando)
v0.3.1 — reescritura más fuerte de marcas de agua estadísticas de Capa B
- La paráfrasis predeterminada de
rewrite_text.pyahora realiza un ataque explícito de elección de palabras + sintaxis (orden de cláusulas, conectores, palabras de transición, límites de oraciones, palabras funcionales) en lugar de una reescritura genérica - Nuevo
--tactic humanize: pasada zero-shot de "escribir como un humano" dirigida a frases formulaicas de estilo IA - Nuevo
--tactic code: reescribe comentarios, docstrings y literales de cadena, y renombra identificadores locales preservando el comportamiento y los nombres de API públicos - La pasada estructural ahora emite "prosa humana natural y variada" en lugar del típico "estilo profesional claro" de IA
- Nuevo
--temperature(predeterminado0.9) para backends Ollama y compatibles con OpenAI - Nuevo
--candidates N: genera N reescrituras y selecciona la más divergente léxicamente (distancia Jaccard de bigramas) con una protección contra deriva de longitud - Higiene de modelos más fuerte: preferir modelos locales de pesos abiertos y evitar cualquier proveedor con marcas de agua conocidas, no solo el origen sospechado
- El informe de riesgo residual ahora distingue texto corto/altamente predecible (menor riesgo) de prosa larga de alta entropía (mayor riesgo)
- Documentación actualizada en
SKILL.md,removal-matrix.mdyvendor-notes.md; las pruebas cubren nuevos prompts, puntuación de divergencia y selección de candidatos
v0.3.0 — puntuación opcional de píxeles SynthID
- Scorer opcional de SynthID en el dominio de píxeles mediante un checkout externo de
aloshdenny/reverse-SynthID(score_synthid.py); expuesto eninspect_image.py/clean_image.pyconREVERSE_SYNTHID_DIRo--synthid-dir - Bootstrap
setup_synthid.sh(dependencias solo del scorer;--fullinstala los requisitos upstream);Dockerfile.synthidmásmake docker-synthid-build/docker-synthid-help - Objetivos
smoke-synthidybootstrap-synthiddel Makefile - Pruebas para el adaptador del scorer, ruta de CLI no disponible, análisis JSON y errores en tiempo de ejecución
- Documentación: solo detección/puntuación (sin eliminación de píxeles); el código upstream no se incluye y permanece bajo su Licencia de Investigación no comercial
v0.2.0 — corrección de falso positivo de c2patool
image_meta.py:has_manifestya no marcaError: No claim found/No JUMBF data foundcomo un manifiesto (error de precedencia de operadores: los marcadores negativos ahora vetas todas las ramas positivas)- Nuevo
tests/test_c2patool_report.py(4 casos: sin claim, sin JUMBF, manifiesto genuino, herramienta ausente) - Documentación: enlaces de
c2patoolcorregidos (el repositorio se movió acontentauth/c2pa-rs); añadido un descargo de responsabilidad sobre el coste de calidad de la eliminación de marcas de agua de texto
v0.1.0 — pulido del empaquetado + honestidad de procedencia
Makefile(test/smoke/install-skill) ypytest.ini- Muestras de fixtures para Markdown, HTML, SVG; prueba de limpieza degradada de PDF
- Documentación: modelo industrial de dos capas (C2PA de enlace fuerte vs enlace suave / SynthID-media)
- Tabla de riesgo residual del README + enlaces a herramientas de verificación externas
- Referencia: guía C2PA/SynthID del Institute of AI PM
- Marcas de agua de enlace suave y de píxeles/audio/vídeo explícitamente fuera de alcance en skill/matrix/ethics
v0.0.1 — lanzamiento inicial multi-proveedor
- Skill de agente
remove-ai-marks(reemplaza aremove-claude-marks, solo para Claude) - Capa A: Unicode invisible / bidi / caracteres de etiqueta / homoglifos de espacio (
inspect_text/clean_text) - Capa B: guía de reescritura +
rewrite_text.pyopcional (print-prompt, Ollama, compatible con OpenAI) - Archivos: eliminación de metadatos C2PA/IA para PNG, JPEG, SVG, PDF, DOCX, ODT, HTML, Markdown
inspect_file.py/clean_file.pyunificados- Documentación multi-proveedor (Claude, Gemini/clase SynthID, OpenAI, LLM abiertos)
- Scripts con prioridad a la biblioteca estándar;
c2patool/exiftoolopcionales
Licencia
MIT — ver LICENSE.
Bibliografía
- 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 (documentación de la API de Gemini)
- C2PA / c2patool
- Kirchenbauer et al., A Watermark for Large Language Models
- Evseev, D. (Arbitration City), Accurate, Costless, and Invisible AI Text Watermarking for Self-Hosted AI Inference (informe técnico, agosto de 2026) — marcas de agua Gumbel con clave incluidas en el motor de código abierto arbi-serve, con detección de prueba exacta y soporte de decodificación especulativa — PDF
- THU-BPM/MarkLLM (kit de herramientas unificado para evaluar algoritmos de marcas de agua de LLM)
- Pan et al., MarkDiffusion: An Open-Source Toolkit for Generative Watermarking of Latent Diffusion Models (JMLR) — el kit de herramientas de incrustación que envuelve el arnés opcional de marcas de agua de imagen de este repositorio — código, documentación
- Zhang et al., Watermarks in the Sand: Impossibility of Strong Watermarking for Generative Models (ICML 2024)
- Sander et al., Watermarking Makes Language Models Radioactive — las marcas de agua sobreviven al ajuste fino y marcan los modelos posteriores entrenados con datos marcados
- Pan et al., Can LLM Watermarks Robustly Prevent Unauthorized Knowledge Distillation? — procedencia basada en marcas de agua y protección contra la destilación de conocimiento
- google-deepmind/synthid-text (referencia de investigación; no se usa para detección aquí)
- aloshdenny/reverse-SynthID (referencia de investigación)
- ETH Zurich SRI, Probing SynthID (blog de investigación sobre la detectabilidad de las marcas de agua SynthID)
- Liu et al., Image Watermarks are Removable Using Controllable Regeneration from Clean Noise (ICLR 2025) — el método de regeneración de píxeles que implementa el backend opcional CtrlRegen — código
- Kassis & Hengartner, UnMarker: A Universal Attack on Defensive Image Watermarking (arXiv:2405.08363; IEEE S&P 2025) — un ataque universal de marcas de agua comparado con una métrica diferente a la de CtrlRegen
- Goonatilake & Ateniese, Removing the Watermark Is Not Enough: Forensic Stealth in Generative-AI Watermark Removal (arXiv:2605.09203) — motiva el valor predeterminado de intensidad conservadora: la eliminación aún puede dejar rastros forenses
- mertizci/noai-watermark (kit de herramientas CLI/Python para la eliminación de SynthID/StableSignature/TreeRing y la eliminación de metadatos de IA)
- 0xROOTPLS/DeSynth (eliminación de SynthID para imágenes de OpenAI/Google)
- Institute of AI PM, AI Content Provenance and Watermarking: The PM's Guide to C2PA and SynthID (modelo industrial de dos capas: C2PA + marca de agua imperceptible / enlace suave; contexto de SB 942 / EU AI Act Art. 50)