
Um aplicativo com foco em privacidade que remove marcas d'água de IA de conteúdo que você possui.
_ _ _ ____ ___ ____ ____ _ _ ____ ____ _ _ ____ ____ ____ _ _ ____ _ _ ____ ____
| | | |__| | |___ |__/ |\/| |__| |__/ |_/ [__ __ |__/ |___ |\/| | | | | |___ |__/
|_|_| | | | |___ | \ | | | | | \ | \_ ___] | \ |___ | | |__| \/ |___ | \
Skill de agente + serviço Python stdlib para remover marcas de proveniência de IA de múltiplos fornecedores de texto e arquivos — para privacidade e higiene em conteúdo que você possui. O skill é um cliente leve: ele aciona a maquinaria via HTTP, então o host do agente não precisa de Python.
Fornecedores / ecossistemas (nível de classe): Claude, Gemini / SynthID-Text, superfícies de proveniência da OpenAI, marcas open-LLM estilo Kirchenbauer (green-list) e keyed-Gumbel / EXP (Aaronson).
Última versão: v0.7.0
Caminho do skill: skills/remove-ai-marks/
Caminho do serviço: service/
(migração: anteriormente remove-claude-marks; alias de barra /remove-claude-marks ainda documentado)
O skill não inclui código — ele chama o serviço via HTTP. Instale o skill (apenas markdown) e inicie o serviço, depois defina WATERMARKS_SERVICE_URL se não for http://127.0.0.1:8765.
No Claude Code, a rota mais rápida é o marketplace de plugins incluído — sem clonar, e ele se atualiza no local. Em todos os outros lugares, um instalador cobre todos os hosts suportados (Python 3.10+ stdlib, sem dependências):```bash python3 install_skill.py --skill remove-ai-marks --target claude-code
| Host | Target | Lands in |
| --- | --- | --- |
| Claude Code (pessoal) | `--target claude-code` | `~/.claude/skills/<skill>` (honors `CLAUDE_CONFIG_DIR`) |
| Claude Code (projeto) | `--target claude-project --project-dir PATH` | `PATH/.claude/skills/<skill>` |
| Cowork, claude.ai, sessões na nuvem, rotinas | `--target cowork` | `dist/<skill>.zip` para enviar em **Customize → Skills** |
| Cursor | `--target cursor` (padrão) | `~/.cursor/skills/<skill>` |
Skills incluídas: `remove-ai-marks` (completa, com suporte de serviço) e
`clean-user-facing-text` (apenas texto, autossuficiente). `--list` as exibe.
Instalações existentes são preservadas a menos que você passe `--force`; a
substituição é preparada primeiro e a instalação anterior é mantida como um
backup com nome exclusivo. `--link` cria symlinks para este checkout em vez de
copiar, de modo que as edições são captadas em tempo real. No Windows, use
`py install_skill.py ...`; o wrapper `install-skill.sh` é fornecido para shells
macOS/Linux.
Antes de escrever qualquer coisa, o instalador valida a skill de acordo com as
regras de empacotamento do [Agent Skills](https://agentskills.io) que os uploads
do claude.ai e a Skills API impõem: frontmatter apenas de especificação (`name`,
`description`, `license`, `compatibility`, `metadata`, `allowed-tools`), um
`name` em minúsculas com hífens de no máximo 64 caracteres correspondendo ao
diretório, uma `description` não vazia de no máximo 1024 caracteres. O pacote
Cowork adicionalmente precisa caber no limite de upload de 30 MB, o que o
empacotador impõe.
### Limpeza automática via hook (determinística)
Uma skill é uma instrução: o modelo decide se deve invocá-la, e o modelo é a
coisa que produz as marcas. Um **hook** é executado pelo harness em cada chamada
de ferramenta correspondente, sem necessidade de cooperação. Isso torna o hook a
metade determinística deste fluxo de trabalho.
O plugin registra um hook `PostToolUse` em `Write|Edit|MultiEdit|NotebookEdit`
que executa [`service/scripts/hook_written_file.py`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/service/scripts/hook_written_file.py)
contra o arquivo que o agente acabou de escrever. Dois modos, seguindo a
convenção de pré-commit de verificação por padrão:
| Modo | Comportamento |
| --- | --- |
| `check` (padrão) | Relata marcas de proveniência, deixa o arquivo intacto. Os achados vão para o modelo (exit 2), para que ele possa oferecer limpá-los. |
| `clean` | Remove as marcas no local, depois informa ao modelo que o arquivo em disco mudou. |
Defina o modo nas configurações do plugin (**Hook mode** em `/plugin manage`,
lido pelo hook como `CLAUDE_PLUGIN_OPTION_HOOK_MODE`), ou com
`WATERMARKS_HOOK_MODE=clean` no ambiente. O comando do hook deliberadamente
**não** interpola `${user_config.hook_mode}`: o Claude Code se recusa a executar
um hook que referencia uma opção que o usuário nunca abriu `/plugin manage` para
definir — um `default` declarado não o satisfaz — então interpolá-lo significaria
que o hook silenciosamente nunca seria executado em uma instalação nova. A
detecção reutiliza `scan_file` / `is_actionable` do `audit_lib`, de modo que o
hook, o gate de pré-commit e a exportação SARIF do CI concordam sobre o que conta
como acionável; a limpeza chama `clean_file.py`, então nenhuma lógica de limpeza
é duplicada. O modo `clean` escreve em um arquivo temporário irmão e troca apenas
em caso de diferença real, então arquivos que já estavam limpos mantêm seu mtime
e não reacionam os observadores de arquivos.
Sem o plugin, conecte-o você mesmo em `~/.claude/settings.json` (ou em um
`.claude/settings.json` do projeto):```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
}
]
}
]
}
}
No Windows, substitua python3 por py.
O que um hook não pode fazer. Nenhum hook pode reescrever a mensagem de chat do assistente
antes que você a leia. O hook Stop do Claude Code recebe last_assistant_message
como somente leitura, e não há filtro de pré-envio para respostas finais — o mesmo limite
que este projeto já documenta para regras do Cursor. Portanto, a garantia determinística
cobre arquivos que o agente escreve, além do
gate de pré-commit para qualquer coisa a caminho do git. Texto que
só existe no transcript do chat ainda depende do fluxo de trabalho da skill,
que é baseado em instruções do modelo e, portanto, best-effort.
O repositório também é um plugin do Claude Code e um
marketplace de plugin único (.claude-plugin/), então ambas as skills instalam e atualizam em dois
comandos, sem necessidade de clone ou script:```
/plugin marketplace add guillaumemeyer/watermarks-remover
/plugin install watermarks-remover@watermarks-remover
As skills então carregam com namespace: `/watermarks-remover:remove-ai-marks` e
`/watermarks-remover:clean-user-facing-text` (o `/remove-ai-marks` simples também
funciona quando nada mais reivindica o nome). `/plugin marketplace update
watermarks-remover` puxa versões posteriores. O mesmo funciona a partir da CLI com
`claude plugin marketplace add …` / `claude plugin install …`, e a partir de um
checkout local passando um caminho em vez de `owner/repo`.
Mantenedores: `make plugin-validate` executa `claude plugin validate . --strict`
contra ambos os manifestos; `tests/test_plugin_manifest.py` cobre os mesmos arquivos
sem precisar da 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
O Claude Code carrega skills pessoais e de projeto sem reiniciar; /skills
lista o que foi carregado. Invoque com /remove-ai-marks ou peça para “remover
marcas de IA / C2PA / marcas do Claude / texto da classe SynthID.” Uma instalação
de projeto também é o que as sessões na nuvem
leem, já que elas clonam o repositório e carregam seu .claude/skills/.
As sessões do Cowork não leem ~/.claude/skills na sua máquina — elas carregam
as skills habilitadas para a sua conta claude.ai, sincronizadas quando a sessão inicia.
Então instale lá enviando um bundle:```bash
python3 install_skill.py --skill remove-ai-marks --target cowork
Em seguida, no aplicativo Claude Desktop, abra **Customize → Skills → Add** e faça o upload
do zip (as mesmas configurações de skill no claude.ai também funcionam). O pacote é
reproduzível e contém um único diretório de nível superior `remove-ai-marks/` com
`SKILL.md` em sua raiz, que é o layout que o upload espera.
A acessibilidade do serviço importa mais aqui do que em uma instalação local: a skill é um
cliente HTTP leve, então a sessão deve ser capaz de alcançar `WATERMARKS_SERVICE_URL`.
Sessões do Cowork que rodam localmente na sua máquina alcançam um `make serve` local;
sessões e rotinas na nuvem rodam remotamente e precisam de uma URL de serviço alcançável a partir
de lá (e `WATERMARKS_SERVER_API_KEY` definida nela). Se você quer uma skill sem
serviço algum, faça o upload de `clean-user-facing-text` em vez disso — ela é somente texto e
inclui seus próprios scripts:```bash
python3 install_skill.py --skill clean-user-facing-text --target cowork
mkdir -p .grok/skills ln -sfn "$(pwd)/skills/remove-ai-marks" .grok/skills/remove-ai-marks
mkdir -p ~/.grok/skills ln -sfn "$(pwd)/skills/remove-ai-marks" ~/.grok/skills/remove-ai-marks
### Skill opcional somente texto
[`skills/clean-user-facing-text/`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/skills/clean-user-facing-text) é uma
skill autocontida para manuscritos autorizados, documentação e textos
para web. Ela exclui ferramentas de imagem, C2PA, serviço e modelo
externo, e executa seus próprios scripts Layer A vendorizados em vez de
chamar o serviço.```bash
python3 install_skill.py --skill clean-user-facing-text --target claude-code
python3 install_skill.py --skill clean-user-facing-text --target cursor
A invocação de skills é selecionada pelo modelo. Projetos que adotam explicitamente este fluxo de trabalho no Cursor também podem copiar a regra 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 os projetos, coloque a mesma instrução em **User Rules** do Cursor.
As regras melhoram a consistência, mas continuam a ser instruções do modelo; o Cursor não expõe
um filtro determinístico de pré-envio para as respostas finais do chat.
### Iniciar o serviço
O caminho mais rápido é um servidor HTTP local (apenas stdlib do Python 3.10+ — sem dependências, sem Docker):```bash
make serve # http://127.0.0.1:8765
# or directly:
python3 service/scripts/server.py --host 127.0.0.1 --port 8765
Consulte docs/windows-autostart.md para iniciar o serviço automaticamente no login do Windows sem Docker.
Para toda a infraestrutura (core + backends opcionais harness/heavy), consulte Docker / compose abaixo.
Ferramentas de sistema opcionais (usadas automaticamente quando presentes — pré-instaladas na imagem Docker core):
Os scripts principais precisam apenas da stdlib do Python 3.10+. As chamadas de modelo da Camada B são opcionais.
SCRIPTS=service/scripts
python3 "$SCRIPTS/inspect_file.py" draft.md python3 "$SCRIPTS/clean_file.py" draft.md -o draft.cleaned.md python3 "$SCRIPTS/clean_file.py" photo.png -o photo.cleaned.png python3 "$SCRIPTS/clean_file.py" notes.docx -o notes.cleaned.docx
python3 "$SCRIPTS/inspect_text.py" draft.md python3 "$SCRIPTS/clean_text.py" draft.md -o draft.cleaned.md --stats
python3 "$SCRIPTS/rewrite_text.py" draft.md --backend print-prompt --tactic paraphrase
python3 "$SCRIPTS/inspect_image.py" shot.png python3 "$SCRIPTS/clean_image.py" shot.png -o shot.cleaned.png
### Ferramentas de texto recusam entrada binária
`inspect_text.py`, `clean_text.py` e `rewrite_text.py` operam sobre texto. Apontados
para um `.docx`, `.pdf` ou imagem, eles costumavam decodificar os bytes comprimidos e reportar
quaisquer codepoints que surgissem — ruído que acompanha a compressão, não o
conteúdo — e `clean_text.py` então gravava esses bytes corrompidos de volta, destruindo o
arquivo. Agora eles recusam entrada binária e indicam a ferramenta que lida com isso:```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.
A detecção é feita por número mágico mais uma proporção de bytes de controle, portanto texto em codificações diferentes de UTF-8 continua funcionando. --force-text substitui isso em todos os lugares.
classify() rotula bytes que não correspondem a nenhum formato de texto, imagem ou contêiner suportado como unknown — ele não recorre mais a "text". No modo automático, clean_file.py recusa tais arquivos (saída 2, nenhuma saída gravada) em vez de decodificá-los como UTF-8 e gravar de volta bytes corrompidos; --as text ou --force-text são as opções explícitas de adesão. inspect_file.py relata o arquivo como unknown (saída 0), e o serviço HTTP responde a /inspect com kind: "unknown", mas rejeita /clean de formatos desconhecidos (400 — envie um nome de arquivo com uma extensão conhecida, por exemplo, notes.txt).
A mesma maquinaria é executada como um serviço HTTP da biblioteca padrão (service/scripts/server.py) — a interface que a skill usa e a forma como qualquer aplicação web pode integrar sem vendorizar:
Os endpoints de lote executam o mesmo pipeline por arquivo que /inspect, /detect, /clean e /watermark, limitado a WATERMARKS_MAX_BATCH_FILES arquivos por requisição (padrão 50). Uma entrada malformada (base64 inválido, opção desconhecida, formato não reconhecido) aparece como "ok": false daquela entrada com uma string "error" — ela nunca aborta o restante do 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"}"
O serviço encaminha por extensão de ficheiro e depois por magic bytes, pelo que texto / imagem / contentor são detetados automaticamente. Defina `WATERMARKS_SERVER_API_KEY` para exigir `Authorization: Bearer <key>` em cada pedido. Bind apenas em loopback por predefinição (`--host` para substituir); destinado a uma rede fidedigna.
### Deteção de marcas de água (`/detect` e `detect_before` / `detect_after`)
A deteção é um passo separado da limpeza — o serviço nunca chama APIs de
fornecedores a menos que lhe seja pedido:
- **`POST /detect`** executa os detetores de marcas de água configurados num ficheiro.
Texto → detetores de fornecedores + estilometria; imagem → pontuação de píxeis SynthID.
- **`/inspect`** aceita um flag opcional `"detect": true` que acrescenta
os resultados dos detetores ao relatório de texto (e pode alterar `suspicious`).
- **`/clean`** aceita as opções `"detect_before"` / `"detect_after"` para
pontuar a entrada e a saída limpa, para que possa medir o que uma limpeza
alterou efetivamente.
- **`/clean`** executa a reescrita de texto da Camada B após a Camada A **por predefinição** (é
um passo obrigatório para texto). Uma opção **`"strategy"`** (uma lista ordenada
`tactic@intensity`, por exemplo `"[email protected],[email protected]"`) substitui a
predefinição do ficheiro de configuração de estratégia (ver abaixo). Quando o backend/modelo de reescrita
para um passo não está configurado, `/clean` devolve um 400.
Detetores de texto (ver `/capabilities` → `text_detectors`):
Detetores de texto (ver `/capabilities` → `text_detectors`):
| Detetor | Ativado por | Notas |
| --- | --- | --- |
| `markllm` | `MARKLLM_DIR` (checkout no host) | Ferramenta de investigação (esquemas KGW / SynthID), apenas com a mesma configuração — não é um oráculo de fornecedor. |
| `gumbel` | `WATERMARKS_GUMBEL_KEY` | Replay sem modelo com a mesma chave do esquema keyed-Gumbel (Aaronson EXP) (ver `detect_gumbel.py`), apenas stdlib — motores auto-alojados como arbi-serve; apenas com a mesma chave, não é um oráculo de fornecedor. |
| `claude-text` | — (placeholder) | A Anthropic anunciou uma API de deteção de marcas de água; esta interface ativa-se quando for lançada. |
Pontuação de imagens: quando `WATERMARKS_SYNTHID_SCORER_URL` está definido, o serviço
pontua imagens através do sidecar `wr-synthid-score` (perfil heavy); com um
`REVERSE_SYNTHID_DIR` local usa o checkout diretamente. A deteção é fail-soft: detetores não configurados, com timeout ou com erro reportam
`{"available": false, "error": ...}` e nunca bloqueiam a limpeza.
### Geração de marcas de água (`/watermark` e `/watermark/batch`)
Gera texto com marca de água para avaliação de benchmarks e testes de ida e volta.
Quando `WATERMARKS_SYNTHID_TEXT_URL` está definido, o serviço delega a geração ao
sidecar `wr-synthid-text` (perfil harness); com um `MARKLLM_DIR` local usa o
checkout diretamente. Tal como a deteção, a geração é fail-soft: um gerador não configurado
reporta `{"ok": false, "error": ...}`.
## Docker / compose
Imagens publicadas (GHCR):
| Etiqueta da imagem | Conteúdo | Publicada? |
| --- | --- | --- |
| `ghcr.io/guillaumemeyer/watermarks-remover:<tag>` / `:latest` | Serviço HTTP principal + todos os limpadores + exiftool / qpdf / c2patool | Sim |
| `…:markllm-<tag>` / `:markllm-latest` | Ferramenta de marcas de água de texto MarkLLM (upstream Apache-2.0) | Sim |
| `…:markdiffusion-<tag>` / `:markdiffusion-latest` | Ferramenta de imagens MarkDiffusion (upstream Apache-2.0) | Sim |
| `watermarks-remover-ctrlregen:local` | Remoção de píxeis CtrlRegen — **nunca publicada** (`noai-watermark` não inclui LICENSE) | Apenas build local |
| `watermarks-remover-synthid-scorer:local` | pontuador reverse-SynthID — **nunca publicado** (Licença de Investigação não comercial) | Apenas build local (pontuador CLI + sidecar HTTP `wr-synthid-score` opcional sob o perfil `heavy`) |
Compilar e executar o serviço 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
Inicialização de toda a infraestrutura:```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
O compose stack mapeia o serviço principal para `127.0.0.1:8765`. Os serviços persistentes são executados como daemons em segundo plano (`wr-core` e o sidecar `wr-synthid-text` sob o perfil harness). Os restantes serviços harness/heavy são CLIs de execução única — invoque com `docker compose run --rm <service> …` quando precisar de verificação ou trabalho de píxeis.
Valide o stack em execução (apenas código de saída, sem saída em caso de sucesso):```bash
make compose-check # or: ./compose-check.sh
Verifica o wr-core via GET /health e executa cada harness/serviço pesado com --help, exigindo saída 0.
A limpeza de texto requer configuração da Camada B — a reescrita da Camada B é um
passo obrigatório para POST /clean em texto, portanto o serviço core precisa do backend de reescrita
configurado, ou a limpeza de texto retorna HTTP 400. A limpeza de metadados de imagem/container
funciona imediatamente. Para texto, você deve configurar as dependências da estratégia da Camada B:
transformers + roberta-large (para o passo mlm padrão) e
a configuração LLM WATERMARKS_REWRITE_* (para o passo paraphrase):```bash
echo "Hello\u200bWorld\u00ad!" > /tmp/sample.txt
curl -s -X POST http://127.0.0.1:8765/clean -H 'Content-Type: application/json'
-d "{"file": "$(base64 < /tmp/sample.txt | tr -d '\n')", "name": "sample.txt"}"
Idiomas cuja tipografia depende de um espaço não separável (francês `« … »`, o
espaço antes de `; : ! ?`) devem passar `"options": {"normalize_spaces": false}`, o
equivalente HTTP de `clean_text.py --no-normalize-spaces`. Os caracteres invisíveis
ainda são removidos; apenas a reescrita de espaços é ignorada.
Todo o resto é opcional e fica num ficheiro `.env` na raiz do repositório. O `docker compose` **carrega automaticamente o `.env`** e interpola a partir dele as referências `${VAR}` em `compose.yaml` (as exportações da shell têm prioridade sobre o `.env` se ambos estiverem definidos).```bash
cp .env.example .env # then edit
docker compose up -d # picks up .env automatically
.env é gitignored (deny-by-default) — nunca o faça commit. Para execuções da CLI no lado do host (rewrite_text.py, a skill), exporte o mesmo ficheiro para o ambiente:```bash
set -a; . ./.env; set +a; python3 service/scripts/rewrite_text.py /tmp/x.txt -o /tmp/x.rewritten.txt
| Var | Reaches | Purpose |
| --- | --- | --- |
| `WATERMARKS_SERVER_API_KEY` | `wr-core` (via compose `environment`) | 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 |
**A Camada B é obrigatória para a limpeza de texto.** O `/clean` sempre aplica a estratégia padrão (de `config/clean_strategy.json`, `{"default_strategy": "[email protected],[email protected]"}`) a um arquivo de texto após a Camada A, a menos que a requisição passe sua própria opção `"strategy"` (uma lista ordenada de `tactic@intensity`). Um passo de estratégia é `tactic@intensity`; o passo `mlm` precisa de `transformers` + `roberta-large`, e qualquer passo de LLM (`paraphrase`, `humanize`, …) precisa da configuração `WATERMARKS_REWRITE_*`. Se o backend/modelo necessário não estiver configurado — ou nenhuma estratégia estiver disponível — o `/clean` **rejeita a requisição com um 400**. Precedência para o caminho da configuração: flag CLI `--strategy-config` > variável de ambiente `WATERMARKS_CLEAN_STRATEGY_FILE` > o padrão `config/clean_strategy.json`.
As imagens são publicadas automaticamente em tags `v*` via [`.github/workflows/release-images.yml`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/.github/workflows/release-images.yml).
## Pontuação opcional de pixels do SynthID
`inspect_image.py` e `clean_image.py` podem reportar uma pontuação de confiança do SynthID no domínio de pixels quando um checkout externo de
[`aloshdenny/reverse-SynthID`](https://github.com/aloshdenny/reverse-SynthID)
estiver disponível. O scorer **não é incluído**: ele é carregado em tempo de execução a partir do seu checkout, e seu código permanece sob a Licença de Pesquisa não comercial do projeto upstream.
### Opção 1: bootstrap com um único comando (sem 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 aceita --dir PATH, --ref REF e --full (instala o
requirements.txt completo do upstream, que adiciona torch/diffusers para o
bypass do VAE upstream que este projeto não utiliza).
No Windows, use setup_synthid.ps1 (-Dir, -Ref, -Full), que cria o
venv em .venv\Scripts\ — o layout que image_meta.py já procura quando
os.name == "nt".
make docker-synthid-build
docker run --rm
--user "$(id -u):$(id -g)"
--read-only --tmpfs /tmp
-v "$(pwd):/data"
watermarks-remover-synthid-scorer /data/shot.png
A imagem é construída localmente a partir do código-fonte upstream no momento da build. Não é publicada, portanto não redistribui o código upstream.
### Opção 3: sidecar do scorer HTTP (docker compose)
No perfil `heavy`, a stack do compose também executa o scorer como um sidecar HTTP (`wr-synthid-score`) para que o **serviço core publicado** possa pontuar imagens antes/depois da limpeza sem incluir o código upstream não comercial. Aponte o `wr-core` para ele e partilhe uma chave 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
Em seguida, POST /clean com {"options": {"detect_before": true, "detect_after": true}} retorna synthid_before / synthid_after no
relatório, e POST /detect em uma imagem retorna a pontuação do SynthID. Fail-soft:
se o sidecar estiver inativo ou não configurado, os relatórios carregam
{"available": false, "error": ...} e a limpeza ainda é bem-sucedida.
A pontuação V4 usa artifacts/spectral_codebook_v4.npz do checkout upstream
(`220 MB). Isto é apenas detecção/pontuação — não remove marcas d'água
de pixel.
Para marcas d'água de imagem no domínio de pixel (classe SynthID, StegaStamp, Tree-Ring,
StableSignature), um backend externo opcional executa o pipeline CtrlRegen
(ControlNet + DINOv2 IP-Adapter controllable regeneration). O backend é
mertizci/noai-watermark, uma
reimplementação mantida do método ICLR 2025
CtrlRegen com tiling automático.
O backend não está incluído e não possui arquivo LICENSE, portanto é tratado como
all-rights-reserved: é clonado em um commit fixado e carregado em tempo de execução.
Suas dependências fixadas da era de pesquisa (requirements-ctrlregen.txt — por exemplo
transformers==4.37.2, diffusers==0.27.2) carregam avisos publicados e
intencionalmente não estão atualizadas, então só são instaladas dentro do
venv dedicado que este script cria e nunca na imagem principal do serviço;
setup_ctrlregen.sh também reverifica o commit fixado em checkouts
existentes, não apenas em clones novos.
SCRIPTS=service/scripts
"$SCRIPTS/setup_ctrlregen.sh"
NOAI_WATERMARK_DIR=~/noai-watermark
~/noai-watermark/.venv/bin/python "$SCRIPTS/clean_ctrlregen.py" shot.png -o shot.ctrlregen.png
No Windows use `setup_ctrlregen.ps1` (mesmos flags que `-Dir`, `-Ref`, `-Python`);
o venv fica em `.venv\Scripts\`, que o `clean_image.py` já resolve.
Ele sonda os índices publicados de wheels do PyTorch e escolhe o mais alto igual
ou abaixo da versão CUDA que o `nvidia-smi` imprime e que realmente existe — esse número
é o máximo que o *driver* suporta, e drivers são retrocompatíveis, então um
driver que reporta 13.1 (sem `cu131` publicado) instala `cu130`. Abaixo da compute
capability 7.5 ele força `cu126`, o último índice cujas wheels ainda carregam
kernels Maxwell/Pascal/Volta. Ele instala `torch` **e** `torchvision`
juntos a partir desse índice para que a instalação de dependências não os troque por builds
CPU do PyPI, depois verifica após a instalação que `torch.cuda.is_available()`
é true — se uma GPU foi detectada mas o torch acaba CPU-only, o script avisa
em alto e bom som e sai com código não-zero em vez de fingir que a configuração deu certo.
### De `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
Ordem das operações: remoção de metadados primeiro, depois remoção de pixels do CtrlRegen, depois
uma pontuação opcional de reverse-SynthID antes/depois (quando REVERSE_SYNTHID_DIR também está
definido).
A intensidade é conservadora por padrão (--ctrlregen-intensity 0.25), porque
uma intensidade mais alta remove mais marca d'água, mas regenera mais da imagem.
Presets documentados: 0.15 mínimo / 0.25 padrão / 0.35 equilibrado /
0.5 agressivo / 0.7 máximo (o padrão do backend é 0.5). --ctrlregen-steps
tem como padrão 50 (passos de denoising efetivos ≈ passos × intensidade).
O CtrlRegen é um ControlNet de Stable Diffusion 1.5 de 512×512. O backend resolve isso para entradas arbitrárias, então nenhuma tile adicional é exposta aqui:
Imagens muito grandes (por exemplo, 4K) produzem muitos tiles, então as execuções escalam com a contagem de tiles (mais lentas e com maior uso de VRAM). Pré-reduza entradas grandes quando for prático; o tamanho do tile e a sobreposição são hardcoded no upstream e não são expostos como flags.
Espere ~10 GB de downloads de modelos; uma GPU é fortemente recomendada e execuções em CPU
são lentas. Alguns modelos upstream têm acesso restrito, então exporte HF_TOKEN (somente via env —
nunca via argv). clean_ctrlregen.py se recusa a instalar dependências automaticamente; execute
setup_ctrlregen.sh primeiro.
Não há detector local para StegaStamp/Tree-Ring/StableSignature, então o
único sinal local é a pontuação de reverse-SynthID (um substituto). Quando disponível,
clean_image.py --remove-pixel ctrlregen reporta essa pontuação antes/depois; a
verificação oficial do Google SynthID continua sendo a autoridade final.
make docker-ctrlregen-build
docker run --rm -e HF_TOKEN="$HF_TOKEN"
--user "$(id -u):$(id -g)"
-v "$(pwd):/data"
watermarks-remover-ctrlregen /data/shot.png -o /data/shot.ctrlregen.png
## Verificação opcional de marca d'água de texto com MarkLLM
Para **experimentos controlados**, um harness externo opcional envolve o
[`THU-BPM/MarkLLM`](https://github.com/THU-BPM/MarkLLM) (Apache-2.0) para
marcar com marca d'água o texto de teste e redetectá-la após uma reescrita da Camada B — por exemplo, provar que
uma marca KGW (Kirchenbauer, sua linha "open-LLM") ou SynthID-Text (linha Gemini) desaparece sob sua reescrita. É um **harness de verificação, não um oráculo**:
a detecção do MarkLLM só é válida contra a *mesma* configuração de esquema + chaves usadas na
geração, e não pode certificar que um detector de fornecedor falhará.
O backend **não está incluído**. `setup_markllm.sh` clona o upstream em um commit
fixado, cria um venv e instala dependências fixadas (torch + transformers); o
modelo de pontuação (padrão `facebook/opt-1.3b`, Apache-2.0) é baixado do Hugging
Face na primeira execução.```bash
SCRIPTS=service/scripts
# Bootstrap (clones upstream, creates ~/MarkLLM/.venv, installs deps).
"$SCRIPTS/setup_markllm.sh"
# Generate watermarked + unwatermarked sample text under the KGW scheme.
MARKLLM_DIR=~/MarkLLM \
~/MarkLLM/.venv/bin/python "$SCRIPTS/detect_text_watermark.py" watermark prompt.txt \
--scheme kgw -o wm.txt -o2 plain.txt
# Detect the scheme mark in a text file.
MARKLLM_DIR=~/MarkLLM \
~/MarkLLM/.venv/bin/python "$SCRIPTS/detect_text_watermark.py" detect wm.txt --scheme kgw --json
Verificação em torno de uma reescrita da Camada B: passe --markllm-scheme para
rewrite_text.py (com --markllm-dir), e ele registra a detecção do MarkLLM
antes/depois, além de um sinalizador 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
**Reescrita iterativa guiada por deteção:** A Camada B agora reescreve iterativamente e
para assim que uma tentativa passa na avaliação. Cada ronda de avaliação gera
variantes `--candidates` (predefinição **1**, `WATERMARKS_REWRITE_CANDIDATES`)
e `--max-loops` limita quantas rondas são executadas antes de a variante de melhor esforço ser
devolvida (predefinição **1**, `WATERMARKS_REWRITE_LOOPS`). Cada variante é uma
chamada de reescrita mais uma avaliação, e uma ronda termina antecipadamente na primeira tentativa
que o avaliador reporta como não marcada com marca de água — portanto, aumentar `--max-loops` repete
novas variantes até uma avaliação passar (uma reescrita limpa típica custa uma
tentativa). O avaliador é escolhido por prioridade:
1. **MarkLLM** — deteção de investigação com a mesma configuração, quando `--markllm-scheme` é
passado (com `--markllm-dir`). Um slot de detetor de fornecedor é reservado acima do
MarkLLM para o detetor SynthID-text da Google, que a Google retirou da sua API
em Ago 2026 — um futuro endpoint de fornecedor pode ser ligado aí.
2. **divergência lexical bigram-Jaccard** — quando nenhum detetor está configurado; sem
veredicto de aprovação/reprovação, portanto cada tentativa é gerada e a mais lexicalmente
divergente é selecionada (o comportamento original).
`--json-stats` reporta o avaliador, tentativas feitas, aprovação/reprovação e registos
por tentativa:```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"}
}
Um detector que não está configurado, expira ou apresenta erros produz uma
entrada "available": false com um motivo error e nunca falha a
reescrita — essa tentativa simplesmente não pode passar, e o loop recorre à
seleção por divergência lexical. Quando o máximo é esgotado sem uma aprovação, a
tentativa menos marcada (pontuação mais baixa) é retornada como melhor esforço com
uma nota.
Se o backend não estiver configurado ou as suas dependências estiverem em falta, a reescrita prossegue e o relatório indica que a verificação não estava disponível. Uma GPU é recomendada; execuções em CPU funcionam mas são lentas, e o download do modelo é de alguns GB.
Opções de reforço:
--offline no adaptador (ou em qualquer execução do MarkLLM) carrega o modelo de pontuação
apenas da cache do Hugging Face — zero tráfego de rede; falha rapidamente se não estiver em cache.
Código remoto personalizado nunca é executado (o trust_remote_code do transformers
nunca é ativado).WATERMARKS_MARKLLM_RLIMIT_AS=<bytes> (env, POSIX) aplica um limite de espaço de endereçamento
ao subprocesso do detector MarkLLM. Desativado por predefinição porque o torch/CUDA
normalmente precisa de grandes espaços de endereçamento.make docker-markllm-build
docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/data"
watermarks-remover-markllm detect /data/wm.txt --scheme kgw --json
### Verificação de mesma chave do Keyed-Gumbel (Aaronson EXP)
O [relatório técnico da ARBI](https://arbicity.com/news/ai-text-watermarking-for-self-hosted-ai/) descreve a
marca d'água de texto keyed-Gumbel ("exponencial") — agora disponível no motor
open-source arbi-serve (`ARBI_WATERMARK_KEY`) — onde o ruído do amostrador é derivado
de um hash com chave da janela de contexto dos últimos 4 tokens. A detecção é uma
**reprodução sem modelo**: recalcular `u = PRF(Hash(key, window), token)` a partir do
texto apenas e testar a cauda Gamma, portanto não precisa de GPU, modelo ou logits.
Este repositório disponibiliza esse detector como `detect_gumbel.py` (somente stdlib; o valor-p
é a identidade exata da soma de Poisson para uma forma Gamma inteira):```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
Mesma ressalva de honestidade do MarkLLM: isto é um replay com a mesma chave — válido apenas contra a mesma chave, tokenizer e layout de PRF usados na geração, e um resultado negativo não estabelece nada. O layout HMAC-SHA256 aqui é uma instanciação auditável, não bit-compatível com qualquer kernel de engine específico (consulte a docstring do módulo para saber o que adaptar para um replay exato).
Reescrita guiada por deteção: passe --gumbel-key para rewrite_text.py
(env: WATERMARKS_GUMBEL_KEY, preferido) e o ciclo de reescrita iterativo é
conduzido pelo replay Gumbel com a mesma chave — a prioridade do avaliador passa a ser gumbel >
MarkLLM > divergência lexical — com um relatório 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
A chave nunca aparece em estatísticas ou logs. Operadores auto-hospedados que
possuem a chave do seu motor podem verificar que uma reescrita removeu uma marca
Gumbel; todos os outros tratam a Camada B como apenas best-effort.
## Benchmark opcional de remoção de SynthID-text
[`bench_synthid_text.py`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/service/scripts/bench_synthid_text.py) mede quão
eficazmente uma reescrita da Camada B remove marcas d'água da classe
SynthID-text e a que custo. Ele gera amostras com e sem marca d'água com o
esquema SynthID do MarkLLM (detecção com a mesma configuração, com verificação
de sanidade), executa suas variantes de reescrita (tática × tentativas máximas
de reescrita; o loop para antecipadamente ao passar) mais controles (sem
remoção, apenas Camada A, verificação opcional de re-stamp), e grava um
`report.md` / `results.json` / `results.csv` compartilhável. Guia completo:
[`docs/synthid-text-benchmark.md`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/docs/synthid-text-benchmark.md).
Requer um checkout do MarkLLM (`setup_markllm.sh` / `MARKLLM_DIR`) e um
backend de reescrita. **O modelo de reescrita é um LLM que você configura** — o
mesmo backend `rewrite_text.py` que a skill usa. O padrão do MarkLLM
`facebook/opt-1.3b` (`--markllm-model`) é apenas o
gerador/detector de marca d'água; ele nunca reescreve. Configure o modelo de
reescrita via variáveis de ambiente ou flags do benchmark (elas espelham a
[tabela de configuração](#configuration-env-vars-for-docker-compose) acima):
| Variável de ambiente | Flag do benchmark | Padrão | Significado |
| --- | --- | --- | --- |
| `WATERMARKS_REWRITE_BACKEND` | `--rewrite-backend` | `ollama` | `ollama` ou `openai-compatible` |
| `WATERMARKS_REWRITE_MODEL` | `--rewrite-model` | *(obrigatório)* | O LLM que realiza a reescrita (ex.: `llama3.2`, `deepseek-v4-flash`) |
| `WATERMARKS_REWRITE_BASE_URL` | `--rewrite-base-url` | `http://127.0.0.1:11434` | Endpoint; o padrão do Ollama é loopback |
| `WATERMARKS_REWRITE_API_KEY` | `--rewrite-api-key` | — | Chave de API (apenas via env no processo filho, nunca argv) |
| `WATERMARKS_REWRITE_ALLOW_REMOTE=1` | `--rewrite-allow-remote` | desativado | Necessário para enviar conteúdo a endpoints não-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
Use um modelo não-origem para reescrever (não reescreva com o mesmo
modelo com marca d'água que gerou o texto) ou a reescrita pode re-marcar a
saída; --restamp-control mede isso.
Para experimentos controlados em imagens, um harness externo opcional envolve
THU-BPM/MarkDiffusion (Apache-2.0),
um kit de ferramentas de marcação d'água generativa para modelos de difusão latente (ele incorpora marcas
— não as remove). Usamos-o para três coisas:
DiffusionPurification
é exposto como clean_image.py --remove-pixel diffusion, uma
alternativa ao CtrlRegen. É regeneração cega (sem
condicionamento ControlNet), portanto desvia mais o conteúdo da imagem do que o CtrlRegen — intensidade
conservadora por defeito (0.3), tratado como fallback/comparação, nunca uma
garantia.O backend não está incluído. setup_markdiffusion.sh cria um venv e
instala markdiffusion==1.0.2 do PyPI (fixado), com torch instalado a partir do
índice de plataforma correto; --checkout instala um clone editável num commit
fixado. O modelo Stable Diffusion (por defeito
huanzi05/stable-diffusion-2-1-base) é descarregado do Hugging Face na primeira execução.```bash
SCRIPTS=service/scripts
"$SCRIPTS/setup_markdiffusion.sh"
echo "a red fox in snow" > /tmp/prompt.txt
MARKDIFFUSION_DIR=~/markdiffusion
~/markdiffusion/.venv/bin/python "$SCRIPTS/markdiffusion_harness.py" watermark
/tmp/prompt.txt -o wm.png -o2 plain.png --scheme tr --json
MARKDIFFUSION_DIR=~/markdiffusion
~/markdiffusion/.venv/bin/python "$SCRIPTS/markdiffusion_harness.py" purify
wm.png -o wm.purified.png --purification-intensity 0.3 --json
MARKDIFFUSION_DIR=~/markdiffusion
~/markdiffusion/.venv/bin/python "$SCRIPTS/markdiffusion_harness.py" detect
wm.purified.png --scheme tr --detector-type l1_distance --json
Ou execute a purificação como parte do pipeline normal de imagens:```bash
MARKDIFFUSION_DIR=~/markdiffusion \
~/markdiffusion/.venv/bin/python "$SCRIPTS/clean_image.py" shot.png \
-o shot.cleaned.png --remove-pixel diffusion
Os ajustes de hardening espelham o harness do MarkLLM: --offline carrega o modelo apenas do
cache do Hugging Face (zero egress de rede, sem código remoto), HF_TOKEN
é apenas via env (nunca argv), as configurações de algoritmo são limitadas a 1 MiB, e o
subprocesso recebe os mesmos limites de recursos mais altos que o CtrlRegen.
make docker-markdiffusion-build
docker run --rm --user "$(id -u):$(id -g)" -v "$(pwd):/data"
watermarks-remover-markdiffusion detect /data/wm.png --scheme tr --json
A imagem instala um torch de CPU; utilizadores de CUDA devem executar `setup_markdiffusion.sh`
no host. Os downloads de modelos continuam a aceder ao hub HF na primeira execução.
## Matriz de cobertura
| Canal | Claude | Gemini/SynthID | OpenAI | Open-LLM |
| --- | --- | --- | --- | --- |
| Texto Unicode / baseado em edição | Camada A | Camada A | Camada A | Camada A |
| **Texto de amostragem estatística** | Camada B best-effort (costura Claude quando a API de deteção da Anthropic for lançada) | Camada B best-effort (+ harness MarkLLM de mesma configuração; a Google retirou o detetor do fornecedor em agosto de 2026) | Camada B se presente | Camada B best-effort + harness MarkLLM opcional |
| C2PA / metadados de ficheiro | Sim (formatos listados) | Sim quando presente | Sim quando presente | Sim quando presente |
| Marcas de imagem em píxeis | Fora do âmbito | Pontuação SynthID opcional + remoção CtrlRegen (externo); deteção MarkDiffusion de mesmo esquema opcional + remoção DiffusionPurification (externo) | Fora do âmbito | Remoção CtrlRegen / MarkDiffusion opcional (externo) |
| Backdoors de treino | Fora do âmbito | Fora do âmbito | Fora do âmbito | Fora do âmbito |
Detalhes: [`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).
---
## Como funciona a marcação de texto (resumo)
As marcas de água modernas de LLM escondem frequentemente um sinal em **quais tokens são escolhidos** (viés generativo / de amostragem), não apenas em caracteres invisíveis. Esquemas baseados em edição injetam regras Unicode ou de sinónimos. Esquemas de ficheiro anexam **C2PA** ou metadados do gerador.
- A **Camada A** remove portadores Unicode baseados em edição (testável).
- A **Camada B** ataca marcas de água de amostragem através de reescrita pesada (best-effort; ataques padrão da literatura como paráfrase / retro-tradução).
- Os **limpadores de ficheiros** removem C2PA/XMP/props de contentores suportados.
Enquanto os fornecedores não disponibilizarem detetores e chaves públicas, **nenhuma ferramenta pode certificar honestamente** "isto falha a verificação oficial." Os relatórios devem separar o trabalho verificável do best-effort.
Prefira um modelo **não-origem** para a Camada B (não reescreva texto Claude com Claude se está a tentar evitar re-marcação).
---
## Aviso legal: o que custa remover uma marca de água de texto
As marcas de água de texto vivem **na própria formulação**: o sinal está espalhado pelas escolhas de tokens, por isso quase todas as frases carregam um pouco dele. Daí decorrem duas consequências, e são a razão pela qual a Camada B é honestamente descrita como *best-effort* em vez de um apagador mágico.
1. **Remoção significa reformulação, não reestruturação.** Baralhar parágrafos, alterar títulos ou fazer retoques ligeiros mal movem o sinal. Remover uma marca estatística exige reescrever uma fração substancial do texto — frase a frase, não secção a secção.
2. **A reformulação degrada o texto.** Qualquer reescrita substitui as escolhas de palavras originais pelas do modelo de reescrita, o que achata o tom, a voz e a precisão. Em texto de produção (SEO, marketing, trabalho para clientes) essa degradação é real e frequentemente visível para quem mais se importa com a escrita. É como pegar em texto de um modelo de topo e pedir a um modelo menos capaz que o reescreva de raiz: o resultado não pode exceder o teto do modelo de reescrita.
O que leva à pergunta honesta de fechar o círculo:
> Se o plano é reescrever o texto com um modelo mais barato de qualquer forma, porque pagar por um modelo premium em primeiro lugar? Gerar diretamente com o modelo mais barato é mais simples, mais barato e produz o mesmo — ou melhor — resultado final.
A Camada B faz sentido quando quer especificamente o **pensamento e o rascunho** do modelo premium e aceita uma passagem de reescrita para satisfazer um requisito de higiene ou privacidade — não como via barata para texto sem marcas.
**Quando saltar a Camada B:**
- **A qualidade importa mais que a higiene:** use o caminho sem perdas — limpeza Unicode da Camada A mais os limpadores de metadados de ficheiros — e mantenha a prosa original.
- **Reescrever de qualquer forma:** use um modelo **não-origem** (reescrever com o modelo de origem pode re-marcar o texto), e lembre-se de que o risco residual permanece — nenhuma ferramenta pode certificar que um detetor de fornecedor vai falhar.
---
## Formatos de ficheiro
| Formato | Inspecionar | Limpar |
| --- | --- | --- |
| PNG / JPEG / WebP | Chunks C2PA / APP11 / RIFF `C2PA`, pistas XMP de IA | Remover segmentos de metadados |
| AVIF / HEIC | Caixas ISOBMFF `jumb` / XMP `uuid` | Remover caixas |
| BMP | Bytes não-imagem à cauda (sem canal padronizado) | Truncar metadados à cauda, corrigir campo de tamanho do ficheiro |
| GIF | Extensões de aplicação Comment / XMP | Remover comment & XMP, manter loop `NETSCAPE2.0` |
| TIFF (clássico + BigTIFF) | Tags IFD: XMP, EXIF, GPS, IPTC, MakerNote | Remover tags, zerar payloads, manter strips |
| SVG | `<metadata>`, XMP | Remover blocos |
| PDF | Byte/XMP + ferramentas opcionais | **exiftool** depois **qpdf**, depois **ghostscript** para metadados dentro de imagens incorporadas; cada ferramenta em falta degrada uma camada diferente (remoção do documento, reescrita estrutural, imagens incorporadas) |
| DOCX | docProps / customXml | Limpar props, remover customXml |
| EPUB | Metadados OPF, meta/JSON-LD XHTML, media incorporada | Limpar OPF, remover meta XHTML, limpar media + Camada A (ignora partes encriptadas) |
| ODT | meta.xml | Remover meta de gerador / tipo IA |
| HTML | meta, JSON-LD, data-ai* | Remover tags/attrs |
| Markdown | Chaves de IA no frontmatter YAML | Remover chaves + corpo da Camada A |
| MP4 / MOV / M4A / M4V | Caixas ISOBMFF `jumb`/`uuid` (mesmo mecanismo que AVIF/HEIC) + tags de gerador `moov/udta` | Remover caixas |
| WAV | Chunks RIFF `C2PA` / `LIST INFO`, chunk `id3\x20` incorporado | Remover chunks |
| MP3 | Frames ID3v2 (v2.3/v2.4 por frame; v2.2 tag inteira) | Remover frames correspondentes ou a tag inteira |
| FLAC | Manifesto C2PA num frame ID3v2 `GEOB` | Remover o frame correspondente ou a tag ID3v2 inteira |
O suporte FLAC cobre o portador ID3v2 padronizado do C2PA. Blocos de metadados FLAC nativos,
Vorbis Comments e marcas de água no domínio da forma de onda ficam intactos.
#### Porque o PDF precisa de qpdf, não apenas exiftool
O ExifTool escreve PDFs **incrementalmente**. `exiftool -all=` acrescenta um
bloco `%BeginExifToolUpdate` que liberta o objeto Info e remove `/Info` do
trailer — mas os bytes de metadados originais permanecem no ficheiro textualmente, e
o próprio exiftool pode desfazer a edição com `-PDF-update:all=`. O comando sai
com `0`, os visualizadores não mostram metadados, e o ficheiro fica *maior*, o que é o indício.
Para uma ferramenta de remoção de proveniência isso é uma fuga silenciosa, por isso `clean_pdf` segue
a passagem do exiftool com `qpdf --linearize`, que re-serializa o documento
a partir do seu grafo de objetos e remove os objetos agora não referenciados. Sem `qpdf`
instalado a limpeza ainda corre, mas di-lo:```
warning: exiftool PDF edits are incremental — the original metadata bytes
remain recoverable; install qpdf for a structural rewrite
Ambas as passagens acima atuam sobre o documento: o dicionário Info, o pacote
XMP, o grafo de objetos. Nenhuma desce até um XObject de imagem, então um scan ou uma
exportação do Photoshop — uma página que é um grande JPEG — mantém tudo o que a imagem
carrega. Num PDF real exportado pelo Photoshop isso deixa 27 tags no lugar após uma
limpeza "bem-sucedida", IFD0:Software, os timestamps de captura e uma miniatura de
preview entre elas; um manifesto C2PA anexado à mesma imagem também sobrevive.
Então clean_pdf adiciona uma terceira passagem, deep_images, conduzida pelo
pdfwrite do Ghostscript. Ela roda em dois níveis e para assim que o arquivo está limpo:
pdfwrite com pass-through reconstrói o documento a partir do
grafo de objetos enquanto copia os dados comprimidos da imagem byte a byte — verificado
por hash dos streams antes e depois. Isso limpa tudo o que o PDF envolveu em torno da
imagem. O pass-through cobre os codecs que o Ghostscript suporta para isso, JPEG (DCTDecode)
e JPEG2000 (JPXDecode); imagens Flate, CCITT e LZW são decodificadas e recodificadas, o que
é sem perdas na prática para esses codecs, mas não byte a byte idêntico. never é a opção
para um documento cujos streams devem sobreviver intactos.always, qualquer metadado APPn sobrevivente. APP0 (JFIF) e APP2 (ICC) são deixados
em paz — o primeiro é estrutural e o segundo decide como as cores são
lidas. Pixels são gastos com evidência, nunca com suspeita.deep_images aceita auto (padrão: nível 1 apenas quando marcadores sobreviveram à
remoção do documento, depois nível 2 se sobreviverem a isso), always (nível 1 para todo
PDF, escalando para o nível 2 também para EXIF de câmera e editor), lossless (nível 1
apenas — nunca recomprimir, e reportar o que sobreviver através dos campos usuais
still_has_c2pa / post_findings) e never. Um valor não reconhecido é
rejeitado em vez de ser silenciosamente tratado como auto. O relatório diz quais níveis rodaram
via meta.deep_image_pass e meta.images_reencoded, e quando a passagem é
pulada ele nomeia a opção que iria mais longe:```text
deep image pass not needed for AI/C2PA markers; pass deep_images="always"
to also clear non-AI EXIF inside images
Sem o Ghostscript instalado, a limpeza ainda é executada e informa o que não conseguiu
alcançar:```text
warning: metadata inside embedded images left in place; install ghostscript
for the deep image pass
A remoção de marca d'água no domínio de pixels está agora disponível como um backend CtrlRegen externo opcional (ver acima); é um removedor regenerativo, não uma garantia. A vinculação suave C2PA (marca d'água no conteúdo que pode religar um manifesto remoto de Content Credentials após os metadados serem removidos) permanece fora do escopo. Remover o C2PA fortemente vinculado não limpa esses canais.
Esta ferramenta reporta remoções verificáveis (contagens Unicode, ações de metadados) e reescritas da Camada B de melhor esforço. Não pode certificar que os detetores dos fornecedores irão falhar.
Para verificar sinais residuais por conta própria (opcional, externo):
Contexto da indústria de duas camadas (C2PA + marca d'água impercetível): guia do Institute of AI PM.
Verificadores fornecidos pelos fornecedores para verificar se o conteúdo contém marcas de proveniência de IA:
Matriz: skills/remove-ai-marks/references/removal-matrix.md.
Ver skills/remove-ai-marks/references/ethics.md. Para privacidade e investigação sobre o seu conteúdo — não fraude académica ou falsas alegações de “escrito por humano”.
Uso responsável: Este projeto destina-se a conteúdo que possui ou está autorizado a processar. Os utilizadores devem cumprir os regulamentos locais e usá-lo de forma responsável. Os programadores declinam qualquer responsabilidade por potencial uso indevido por parte dos utilizadores.
Projetos de terceiros que envolvem ou complementam este repositório, listados apenas para fins de descoberta. Não são mantidos, endossados ou suportados por este projeto. Este projeto não revê o seu código, não garante o seu comportamento ou garantias, nem assume responsabilidade por qualquer coisa que instale ou execute a partir desta lista. Cada projeto é regido pela sua própria licença, mantenedores e documentação — leia-os antes de o utilizar.
MetaClean é uma aplicação de ambiente de trabalho independente em Rust/Tauri licenciada sob MIT (Windows, macOS, Linux) que fornece uma GUI nativa empacotada para limpeza de metadados por arrastar e largar, com uma bandeja do sistema e integração com o Explorador. É uma base de código separada: não chama o serviço Python deste repositório, e os seus formatos suportados e garantias de limpeza diferem das deste projeto. Consulte o seu README para detalhes.
unmark-web é um cliente web estático independente licenciado sob MIT. Remove marcas Unicode invisíveis de texto e elimina metadados de proveniência de imagens inteiramente no navegador, e pode opcionalmente chamar o serviço HTTP deste repositório para os formatos que não trata localmente. É uma base de código separada e não está afiliada a este projeto; consulte o seu README para âmbito e limites.
DropMarks é uma aplicação macOS SwiftUI independente licenciada sob MIT. Chama o inspect_file.py / clean_file.py deste repositório (e opcionalmente rewrite_text.py) através de um snapshot vendorizado desses scripts stdlib. É uma base de código separada e não está afiliada a este projeto; consulte o seu README para âmbito e limites.
Para registar um projeto aqui, abra um PR adicionando uma entrada curta — nome do projeto, o que envolve ou acrescenta, e uma ligação para o seu próprio repositório. Mantenha as entradas breves e factuais; não reivindique compatibilidade com, ou endosso por, este projeto. Um projeto listado deve basear-se em ou integrar este repositório — por exemplo, chamando o seu serviço ou reutilizando o seu motor de deteção — em vez de apenas abordar o mesmo problema de forma independente. Evite nomes que comecem por ou se assemelhem a watermarks-remover — nomes semelhantes tornam difícil distinguir qual projeto é qual.
O gating de CI já existe (exportação SARIF do audit_dir.py, ver contexto da Matriz de cobertura) — os hooks de pre-commit abaixo detetam a mesma classe de problema mais cedo, antes de um ficheiro marcado ser sequer commitado. Ambos envolvem as CLIs existentes (audit_dir.py / clean_file.py) — sem lógica de deteção separada.```yaml
repos:
`watermarks-remover-check` falha no commit e lista as descobertas; `watermarks-remover-clean` é opt-in e reescreve os arquivos em stage no local (sai com 1 para que você revise o diff e faça o stage novamente — a mesma convenção de hooks de correção automática como `ruff --fix`). Quando o cleaner não consegue processar um arquivo de forma alguma — ele travou, foi morto ou não produziu relatório — `watermarks-remover-clean` nomeia esse arquivo e sai com 3, para que um cleaner que falhou nunca seja confundido com um arquivo já limpo. Execute qualquer um manualmente com `python3 service/scripts/check_staged.py <files...>` / `clean_staged.py <files...>`.
## Tests```bash
python3 -m venv .venv && .venv/bin/pip install pytest
.venv/bin/python -m pytest # or: make test
make smoke # quick CLI smoke on fixtures
/clean, módulo de roubo de marca d'água, remoção de marca d'água de áudio/vídeo e amplitude de benchmark/ferramentasA v0.7.0 traz a reescrita de marca estatística da Camada B para dentro do próprio serviço /clean, impulsionada por uma estratégia configurável e ajustada por benchmark ([email protected],[email protected]). Junto com isso: um módulo de roubo de marca d'água de caixa-preta, remoção destrutiva de marca d'água de áudio e de vídeo quadro a quadro, um benchmark de reescrita substancialmente mais rico e uma pilha de correções de robustez, segurança e ferramentas.
Reescrita da Camada B no serviço
/clean executa a reescrita da Camada B para texto após a Camada A. O padrão vem de config/clean_strategy.json; um options.strategy por requisição o substitui, e /clean rejeita com 400 quando o backend necessário não está configurado (#315). Precedência de configuração: --strategy-config > WATERMARKS_CLEAN_STRATEGY_FILE > config/clean_strategy.json.mlm: mascara uma fração das palavras de conteúdo e preenche com roberta-large — uma edição local não autorregressiva, de modo que a saída mistura o fluxo de tokens original com previsões de MLM mascarado (#311).humanize agora aplica a passagem de habilidade de humanização de forma determinística (aspas retas, sem travessões em/en, colapsos de preenchimento, utilize→use) e nomeia as regras de escrita humana no prompt (#311). ganhou um caminho CLI .Benchmark
Roubo de marca d'água
Áudio / vídeo / imagem
uuid de proveniência de conteúdo C2PA reconhecida em MP4/MOV/AVIF/HEIC (#264).zTXt/iTXt de PNG descomprimido a 1 MiB (#308); remover declarações DOCTYPE/ENTITY de XML em SVG (#288); manter membros binários de DOCX seguros byte a byte (#314); preservar AppVersion de OOXML (#289).Serviço HTTP e CLI
/clean para manter espaços exóticos, espelhando a CLI (#274); /inspect expõe classes de evidência explícitas no payload suspeito (#277); timestamps nos logs de requisição HTTP (#256); encaminhar bytes do payload para a pontuação SynthID HTTP e inspect_* para evitar uma releitura redundante.clean_file.py ganhou -q/--quiet/--only-changed (#254).Skills, plugin e hooks
clean-user-facing-text (#258); lançador de hook PostToolUse tornado multiplataforma (#255); hook de pre-commit trata arquivos não-texto limpos byte a byte idênticos como alterados (#238).Auditoria
audit_dir.py varre arquivos de código-fonte, documentação e i18n que o roteador ignorou (#284); varre .ts/.tsx/.jsx/.gd e alinha a confiança de espaço entre formatos (#273); suporte a audit_website.py --sarif (#194); robustecer backups in-place, status de arquivo limpo, veredito SynthID, ID3v2 truncado e roteamento de zip (#201).Segurança
CI, ferramentas e documentação
Cobertura de formato e contêiner
NETSCAPE2.0 e outros chunks de animação são preservados; metadados de IFD de TIFF (XMP/EXIF/GPS/IPTC/MakerNote) são descartados com payloads zerados e offsets de strip mantidos, tanto para TIFF clássico quanto BigTIFF; metadados à direita de BMP são truncados com o campo de tamanho de arquivo reescrito (#107)docProps de DOCX; podar relacionamentos pendentes após remoção de customXml; executar Camada A sobre o texto do corpo de DOCX/ODT; decodificar entidades XML antes da limpeza da Camada A (#91, #100, #76, #83, #73, #80, #74, #81, #142)Robustez da Camada A (Unicode invisível)
Default_Ignorable reservados sem uso legítimo de intercâmbio (U+2065, U+FFF0–U+FFF8, U+E0000, U+E0080–U+E00FF, U+E01F0–U+E0FFF — reportados como reserved_ignorable), os 66 não-caracteres (U+FDD0–U+FDEF mais U+FFFE/U+FFFF por plano — reportados como noncharacter), e três portadores Default_Ignorable de renderização em branco que o catch-all nunca viu (, , ). Cada um tem a mesma preservação em contexto que seus irmãos já cobertos, de modo que texto de sílaba parcial não é corrompido, e cada um é aplicado tanto ao motor do serviço quanto à cópia leve de skill embarcadaReescrita da Camada B e detecção de marca d'água
--candidates (padrão 1, WATERMARKS_REWRITE_CANDIDATES) e --max-loops (padrão 1, WATERMARKS_REWRITE_LOOPS) limita as rodadas de avaliação, parando assim que uma tentativa passa na detecção. Prioridade do avaliador: MarkLLM (--markllm-scheme) > divergência lexical bigram-Jaccard (fallback). rewrite_text.py --json-stats agora reporta evaluator / max_loops / attempts_made / passed e candidate_scores por tentativa (#153)detect_gumbel.py apenas com stdlib implementa o teste de replay sem modelo (u = PRF(Hash(key, window), token); p-valor exato de cauda Gamma; mascaramento de janela repetida) sem GPU, modelo ou logits. (env , preferido) o torna o avaliador do loop iterativo (prioridade: gumbel > markllm > divergência lexical) e é exposto como em e . Apenas mesma chave — não é um oráculo de fornecedor; a chave nunca é registrada em log (#190)Distribuição: instalações de plugin, hooks e skill
.claude-plugin/plugin.json + marketplace.json), então ambas as skills são instaladas com /plugin marketplace add guillaumemeyer/watermarks-remover e depois /plugin install watermarks-remover@watermarks-remover, e atualizadas no local. make plugin-validate executa claude plugin validate . --strict; tests/test_plugin_manifest.py verifica os manifestos sem a CLIinstall_skill.py ganhou um --target (claude-code, claude-project, cowork, cursor) e um seletor cobrindo ambas as skills distribuídas, além de , e . O alvo constrói um pacote de upload reproduzível (, diretório de skill único de nível superior); cada alvo valida contra as regras de empacotamento do Agent Skills e o limite de upload de 30 MB. Novos alvos : , , , , Serviço HTTP
POST /clean/batch, /inspect/batch (#137) e POST /detect/batch (#151)/clean e usar escritas seguras em av_meta (#150); usar base64 portátil no exemplo curl de /detect (e corrigir a portabilidade de realpath do macOS nos bootstraps, #185)Auditoria / inspeção e segurança
audit_dir.py ganhou concorrência multi-worker e exportação SARIF 2.1.0 (#101, #102)Correções de confiabilidade e correção
--in-place preserva o .bak original; manter evidências coletadas quando um membro de zip posterior falha ao ser lido (#175); contêineres ISOBMFF truncados ainda executam o fallback de varredura de bytes C2PA (#176); distinguir um limpador que falhou de um arquivo já limpo (#159, #161); tratar uma execução falha do c2patool como inconclusiva em vez de "sem C2PA" (#156); validar tipos de opção de limpeza (#111); nunca selecionar automaticamente o dispositivo MPS para detecção de marca d'água de texto (#99); portabilidade macOS — stdout puro --json para o pontuador SynthID e sonda realpath do BSD (#70); corrigir um caminho subprocess_creationflags do Windows em _ghostscript_usable e impedir que processos filhos abram uma janela de console no Windowsbench-synthid-text; simplificar o passthrough de flags para a sonda Ghostscript e limpar noqa desnecessário de clean_text (lint)CI / ferramentas / documentação
watermarks-remover-clean / clean_staged.py): usar digests de conteúdo (SHA-256) e detecção de ação ativa para que arquivos limpos no disco sejam reconhecidos sem exigir re-staging infinito (#173)<AppVersion> intacto em docProps/app.xml durante a limpeza de metadados de DOCX, XLSX e PPTX para satisfazer restrições de esquema ECMA-376 e evitar erros de "conteúdo ilegível" do Microsoft Word/Office (#283)Distribuição de serviço / Docker
skills/remove-ai-marks/) agora é um cliente remoto sem código sobre HTTP; toda a implementação foi movida para service/scripts/ e roda atrás de server.py, um ponto de entrada HTTP da stdlib (/health, /inspect, /clean, /capabilities)service/scripts/server.py expõe o pipeline de limpeza sobre JSON/base64; a robustez espelha as CLIs (limites de tamanho, guarda binária, escritas atômicas, padrão loopback, autenticação bearer opcional WATERMARKS_SERVER_API_KEY)GET /openapi.json serve uma especificação OpenAPI 3.0.3 gerada dinamicamente (construída a partir da tabela de rotas + configuração ao vivo, de modo que nunca se desvia dos endpoints reais); CI a valida com openapi-spec-validatorHarness de marca d'água de imagem MarkDiffusion (opcional)
THU-BPM/MarkDiffusion externo, Apache-2.0): markdiffusion_harness.py com subcomandos watermark / detect / purify para nove esquemas de imagem (Tree-Ring, Ring-ID, ROBIN, WIND, SFW, Gaussian-Shading, GaussMarker, PRC, SEAL)clean_image.py --remove-pixel diffusion executa o ataque de regeneração DiffusionPurification do MarkDiffusion como um motor alternativo de remoção de pixels (intensidade conservadora 0.3 por padrão)setup_markdiffusion.sh (pin do PyPI 1.0.2; clone editável --checkout em commit fixado) + requirements-markdiffusion.txt + Dockerfile.markdiffusion e Makefile bootstrap-markdiffusion / / / Harness de marca d'água de texto MarkLLM (opcional)
THU-BPM/MarkLLM, Apache-2.0): detect_text_watermark.py com subcomandos detect / watermark para esquemas KGW e SynthIDrewrite_text.py --markllm-scheme executa detecção antes/depois em torno de uma reescrita da Camada B e detecção por candidato quando --candidates N>1 (controlado por env; reporta cleared)setup_markllm.sh + requirements-markllm.txt (deps fixadas) + Dockerfile.markllm e Makefile bootstrap-markllm / smoke-markllm / docker-markllm-build / Correções e polimento- Layer B: rewrite_text.py agora envia reasoning_effort: "none" por padrão para backends openai-compatible (--reasoning-effort / WATERMARKS_REWRITE_REASONING_EFFORT; off omite). Modelos de raciocínio como deepseek-v4-flash caso contrário gastam ~100s de chain-of-thought em uma reescrita de uma linha (9.894 vs 12 tokens de conclusão)
requirements-markllm.txt fixava tokenizers==0.23.1, que entra em conflito com transformers==5.15.0 (limita tokenizers<=0.23.0; não existe release 0.23.0) — agora fixado em tokenizers==0.22.2; torch movido para o índice de wheels de CPU (torch==2.13.0.*) para que a imagem seja somente CPU, como Dockerfile.markdiffusionsafetensors==0.4.3, transformers==4.37.2 → tokenizers<0.19) não fornecem wheels para Python 3.14, então a imagem base agora é python:3.11-slim (fixada por digest, multi-arch)Dockerfile.markllm e nunca copiavam para (bug pré-existente) — adicionadoRemoção opcional de pixel CtrlRegen (backend externo)
mertizci/noai-watermark: adaptador clean_ctrlregen.py + bootstrap setup_ctrlregen.sh (commit fixado, sparse checkout, venv, verificação SHA), além de Dockerfile.ctrlregen e make bootstrap-ctrlregen / docker-ctrlregen-build / smoke-ctrlregenclean_image.py --remove-pixel ctrlregen executa strip de metadados → remoção CtrlRegen → pontuação opcional reverse-SynthID antes/depois; inspect_image.py sugere a flag em uma pontuação SynthID alta0.25 (presets 0.15/0.25/0.35/0.5/0.7); o pipeline nativo 512×512 é auto-tileado pelo backend para imagens maiores; o subprocesso torch recebe limites de recursos mais altos, sobrescrevíveis por envnoai-watermark não inclui arquivo LICENSE (tratado como todos os direitos reservados), e seus caminhos de código de auto-instalação/reinício são contornados usando diretamenteConfiança de achados e auditorias agregadas
confirmed / probable / informational / likely_false_positive, expostos em JSON de texto/imagem/container e relatórios humanosaudit_dir.py (árvore recursiva) e audit_website.py (descoberta de sitemap + crawl) agregam relatórios; documentados em SKILL.mdCorreções de falsos positivos
docProps/customXml, não o corpo visível (#14)VS16/ZWJ após uma base de emoji; nova flag paranoica --strip-emoji-glue (#22)Suporte a Windows
preexec_fn e os.fchmod exclusivos de POSIX para que gravações e ferramentas opcionais rodem no Windows (#15, #23)Docs e cadeia de suprimentos
safe_write_bytes / safe_write_text), recusa destinos com symlink e cria backups .bak pelo mesmo caminho seguro — symlinks pré-colocados (por exemplo, em /tmp ou diretórios de download) não podem mais redirecionar uma gravação limpa para um arquivo arbitráriorewrite_text.py: redirecionamentos são recusados de imediato, então uma chave de API no cabeçalho Authorization nunca pode ser reenviada a um host não validado; endpoints não-loopback são negados por padrão (opt-in com --allow-remote ou WATERMARKS_REWRITE_ALLOW_REMOTE=1); apenas esquemas http(s) são aceitos; --api-key foi removido — chaves são apenas via env com WATERMARKS_REWRITE_API_KEYRLIMIT_AS/ aplicados a subprocessos exiftool/c2patool/SynthID (todos os limites sobrescrevíveis por env)rewrite_text.py agora realiza um ataque explícito de escolha de palavras + sintaxe (ordem de orações, conectores, palavras de transição, limites de frase, palavras funcionais) em vez de uma reescrita genérica--tactic humanize: passo zero-shot "escreva como um humano" visando frases formulaicas de estilo IA--tactic code: reescreve comentários, docstrings e literais de string, e renomeia identificadores locais preservando comportamento e nomes de API públicos--temperature (padrão 0.9) para backends Ollama e OpenAI-compatible--candidates N: gera N reescritas e seleciona a mais lexicalmente divergente (distância Jaccard de bigramas) com uma proteção contra desvio de comprimentoSKILL.md, removal-matrix.md e vendor-notes.md; testes cobrem novos prompts, pontuação de divergência e seleção de candidatosaloshdenny/reverse-SynthID (score_synthid.py); exposto em inspect_image.py / clean_image.py com REVERSE_SYNTHID_DIR ou --synthid-dirsetup_synthid.sh (dependências apenas do scorer; --full instala os requisitos upstream); Dockerfile.synthid mais make docker-synthid-build / docker-synthid-helpsmoke-synthid e bootstrap-synthid no Makefileimage_meta.py: has_manifest não sinaliza mais Error: No claim found / No JUMBF data found como um manifesto (bug de precedência de operadores: os marcadores negativos agora vetam todos os ramos positivos)tests/test_c2patool_report.py (4 casos: sem claim, sem JUMBF, manifesto genuíno, ferramenta ausente)c2patool corrigidos (repositório movido para contentauth/c2pa-rs); adicionado um aviso sobre o custo de qualidade da remoção de marca d'água de textoMakefile (test / smoke / install-skill) e pytest.iniremove-ai-marks (substitui remove-claude-marks, exclusivo do Claude)inspect_text / clean_text)rewrite_text.py opcional (print-prompt, Ollama, OpenAI-compatible)inspect_file.py / clean_file.py unificadosc2patool / exiftool opcionaisMIT — veja LICENSE.
| Camada | Alvo | Como |
|---|
| A | Unicode invisível, espaços exóticos, bidi, caracteres de tag | Scripts Python determinísticos |
| B | Marcas d'água estatísticas (amostragem de tokens) em texto | Reescrita pelo agente + hook opcional rewrite_text.py |
| Arquivos | C2PA / EXIF / XMP / propriedades 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 |
| Ferramenta | Função |
|---|
c2patool | Inspecionar manifestos C2PA |
exiftool | Remoção de metadados residuais (esp. PDF) |
qpdf | Reconstrução estrutural de PDF — obrigatório para uma remoção real de PDF (ver abaixo) |
| Método | Caminho | Corpo | Retorna |
|---|
| GET | /health | — | {"ok": true, "version": ...} |
| GET | /capabilities | — | ferramentas / backends opcionais utilizáveis (cada ferramenta tem sua versão sondada, não apenas encontrada no PATH) |
| GET | /openapi.json | — | especificação OpenAPI 3.0.3 gerada dinamicamente |
| POST | /inspect | {"file": "<base64>", "name": "notes.md"} | {"ok", "kind", "suspicious", "report"} |
| POST | /detect | {"file": "<base64>", "name": "notes.txt"} | {"ok", "kind", "detections": [...]} |
| POST | /clean | {"file": "<base64>", "name": "notes.md", "options": {...}} | {"ok", "kind", "cleaned": "<base64>", "report"} |
| POST | /watermark | {"text": "...", "keys": [118, 504, ...], "options": {...}} ou {"file": "<base64>", ...} | {"ok", "kind", "watermarked_text", "report": {"scheme_used", ...}} |
| POST | /inspect/batch | {"files": [{"file": "<base64>", "name": "notes.md"}, ...]} | {"ok", "results": [{"name", "ok", "kind", "suspicious", "report"}, ...]} |
| POST | /detect/batch | {"files": [{"file": "<base64>", "name": "notes.txt"}, ...]} | {"ok", "results": [{"name", "ok", "kind", "detections", "report"}, ...]} |
| POST | /clean/batch | {"files": [{"file": "<base64>", "name": "notes.md", "options": {...}}, ...]} | {"ok", "results": [{"name", "ok", "kind", "cleaned", "report"}, ...]} |
| POST | /watermark/batch | {"files": [{"text": "...", "keys": [...]}, {"file": "<base64>"}, ...]} | {"ok", "results": [{"name", "ok", "kind", "watermarked_text", "report": {"scheme_used", ...}}, ...]} |
| Canal | O que removemos | O que pode permanecer | Verificação externa (exemplos) |
|---|
| C2PA fortemente vinculado / EXIF / XMP | Sim | Marcas suaves / de pixels | c2patool, Content Credentials verify |
| Mídia da classe SynthID | Remoção opcional de pixels (CtrlRegen externo); pontuação local caso contrário | Marca d'água de áudio/vídeo; marca d'água residual de pixels após remoção | Ferramentas do fornecedor (por exemplo, Google SynthID / detetor Vertex onde oferecido); pontuador local opcional reverse-SynthID |
| Texto estatístico | Reescrita de melhor esforço | Marcas fortes após edição leve | Sem detetor universal público; ferramentas do fornecedor quando disponíveis |
| Opção | Remove | Notas |
|---|
| Limpeza Unicode (Camada A) | ZWSP, bidi, tags, espaços exóticos, … | Predefinição segura para texto |
| Reescrita (Camada B) | Marcas estatísticas de tokens (melhor esforço) | Sempre oferecida pela skill; custa estilo — ver Aviso legal |
| Remoção de contentor/metadados | Proveniência do ficheiro | Ver tabela de formatos |
| Remoção de pixels CtrlRegen (opcional) | Marcas de imagem no domínio de pixels (classe SynthID, StegaStamp, Tree-Ring, StableSignature) | Backend externo; computação pesada; intensidade predefinida conservadora |
| Remoção de pixels DiffusionPurification (opcional) | Marcas de imagem no domínio de pixels (classe Tree-Ring) | Backend MarkDiffusion; regeneração cega (mais desvio que o CtrlRegen); intensidade predefinida conservadora |
| Modelos locais de pesos abertos | Evitar re-marcação com o modelo de origem | Alternativa operacional |
rewrite_text.py--strategyCfU+180FU+3164U+FFA0U+13430–U+1343F), controles de taquigrafia Duployan (U+1BCA0–U+1BCA3) e controles musicais de beam/tie/slur/phrase (U+1D173–U+1D17A) agora são preservados quando adjacentes ao seu próprio script e ainda removidos (e sinalizados) quando flutuando entre texto não relacionado; o modo paranoico --strip-emoji-glue ainda os remove em todos os lugaresrewrite_text.py --gumbel-keyWATERMARKS_GUMBEL_KEYgumbel/capabilities/detectparaphrase:3; relatório e CSV carregam tentativas por documento (colunas mean_attempts / att, attempts / evaluator / passed); --rewrite-loops espelha --max-loops--skill--list--linkCLAUDE_CONFIG_DIRcoworkdist/<skill>.zipmakeinstall-claude-code-skillinstall-claude-code-text-skillinstall-claude-project-skillpackage-cowork-skillpackage-cowork-text-skillPostToolUse (hooks/hooks.json + service/scripts/hook_written_file.py): após o agente escrever um arquivo, o harness executa o hook independentemente de o modelo cooperar. check (padrão) reporta marcas ao modelo; clean as remove no local e informa ao modelo que o arquivo mudou, trocando apenas em uma diferença real para que arquivos limpos mantenham seu mtime. O modo vem da configuração hook_mode do plugin ou de WATERMARKS_HOOK_MODE; a detecção reutiliza audit_lib.scan_file / is_actionable, de modo que o hook, o portão de pre-commit e a exportação SARIF de CI concordam. Um hook ainda não pode reescrever a mensagem de chat do assistente — tal ponto de hook não existe — então esse caminho permanece best-effortclean-user-facing-text não nomeia mais o Cursor como o único hostservice/Dockerfilecompose.yaml sobe toda a infraestrutura (core sempre; markllm / markdiffusion atrás de profile: harness; ctrlregen / synthid atrás de profile: heavy como builds apenas locais); serviços são prefixados com wr-; serviços harness/heavy têm como padrão command: ["--help"] para que docker compose up --profile harness --profile heavy saia de forma limpa (CLIs one-shot são executadas com docker compose run); novos make compose-check / compose-check.sh validam a stack em execução (apenas código de saída).github/workflows/release-images.yml publica imagens core, markllm, markdiffusion em tags v*; ctrlregen / synthid nunca são publicadas (licenciamento upstream).env.example + guia de configuração do serviço; docker compose carrega .env automaticamente; .env está no gitignore (negar por padrão).gitignore e service/.dockerignore agora são negar por padrão — apenas caminhos explicitamente permitidos podem ser commitados ou enviados em um contexto de build (contextos de imagem enviam apenas service/scripts/, que é tudo o que os Dockerfiles fazem COPY)tests/test_http_server.py (13 casos) para o serviço HTTP; todas as suítes redirecionadas para service/scripts/smoke-markdiffusiondocker-markdiffusion-builddocker-markdiffusion-helptests/test_markdiffusion_harness.py) — sem torch na CI; documento de referência references/markdiffusion.mdremoval-matrix.md, markdiffusion.mddocker-markllm-help--offline (sem egress HF, sem código remoto), limite de 1 MiB para config, WATERMARKS_MARKLLM_RLIMIT_AS opcional no subprocesso de reescrita, torch fixado no Dockerfile e verificação de SHA do clone em Dockerfile.markllmtests/test_markllm_detect.py, 21 casos) — sem torch na CI; ressalva de harness de verificação (apenas mesma configuração, não é um oráculo de detector de fornecedor) documentada no README, SKILL.md, removal-matrix.md, vendor-notes.mdDockerfile.markdiffusioncommon.py/appC2PA, XMP, EXIF e perfil ICC (#37)NETSCAPE2.0 é preservado; metadados IFD do TIFF (XMP/EXIF/GPS/IPTC/MakerNote) são descartados com payloads zerados e offsets de strip mantidos, tanto para TIFF clássico quanto BigTIFF; metadados à direita do BMP são truncados com o campo de tamanho do arquivo reescrito--force-text sobrescreve (#24)--json não suprime mais o código de saída de sinal residual (#30)inspect_file imprime o nome do arquivo em sua saída (#50)CtrlRegenEngineRLIMIT_FSIZEpermissions: contents: read, deps de dev fixadas (requirements-dev.txt), um passo pip-audit e um novo workflow CodeQL; a imagem Docker agora roda como usuário sem privilégios com pip fixado