Voltar às atualizações
New releaseAug 30, 2026

watermarks-remover v0.5.0

Um aplicativo com foco em privacidade que remove marcas d'água de IA de conteúdo que você possui.

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

watermarks-remover

CI Release Stars Forks

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.

CamadaAlvoComo
AUnicode invisível, espaços exóticos, bidi, caracteres de tagScripts Python determinísticos
BMarcas d'água estatísticas (amostragem de tokens) em textoReescrita pelo agente + hook opcional rewrite_text.py
ArquivosC2PA / EXIF / XMP / propriedades de documentosPNG, JPEG, WebP, AVIF, HEIC, BMP, GIF, TIFF, SVG, PDF, DOCX, XLSX, PPTX, EPUB, ODT, HTML, Markdown, MP4/MOV/M4A/M4V, WAV, MP3, FLAC

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)

Instalação (skill de agente)

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.

Plugin do Claude Code (marketplace)

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/.

Cowork (e claude.ai, sessões na nuvem, rotinas)

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

writes dist/remove-ai-marks.zip (make package-cowork-skill)

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

Grok```bash

Grok Build / project-local

mkdir -p .grok/skills ln -sfn "$(pwd)/skills/remove-ai-marks" .grok/skills/remove-ai-marks

User-global Grok

mkdir -p ~/.grok/skills ln -sfn "$(pwd)/skills/remove-ai-marks" ~/.grok/skills/remove-ai-marks

### Skill 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

Windows (sem Docker)

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):

FerramentaFunção
c2patoolInspecionar manifestos C2PA
exiftoolRemoção de metadados residuais (esp. PDF)
qpdfReconstrução estrutural de PDF — obrigatório para uma remoção real de PDF (ver abaixo)

Os scripts principais precisam apenas da stdlib do Python 3.10+. As chamadas de modelo da Camada B são opcionais.

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

### 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.

Formatos não reconhecidos nunca são limpos automaticamente

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).

Serviço HTTP

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:

MétodoCaminhoCorpoRetorna
GET/health{"ok": true, "version": ...}
GET/capabilitiesferramentas / backends opcionais utilizáveis (cada ferramenta tem sua versão sondada, não apenas encontrada no PATH)
GET/openapi.jsonespecificaçã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", ...}}, ...]}

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.

Configuração (variáveis de ambiente para docker compose)

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".

Opção 2: build local com 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

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.

Remoção opcional de pixel CtrlRegen

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.

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

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).

Tamanho da imagem (limite nativo de 512×512)

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:

  • ≤512 px: passagem única — center-crop/redimensiona para 512, regenera, redimensiona de volta.
  • >512 px: tiling automático com sobreposição (tiles de 512 px, sobreposição de 192 px), largura/altura alinhadas a múltiplos de 8, depois costuras com blend de cosseno.
  • Qualquer caminho: a saída é redimensionada para o tamanho original e com cores correspondentes à imagem original.

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.

Computação, modelos com acesso restrito e verificação

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.

Docker```bash

make docker-ctrlregen-build docker run --rm -e HF_TOKEN="$HF_TOKEN"
--user "$(id -u):$(id -g)"
-v "$(pwd):/data"
watermarks-remover-ctrlregen /data/shot.png -o /data/shot.ctrlregen.png

## Verificaçã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.
  • Os ficheiros de configuração estão limitados a 1 MiB; o checkout upstream e a imagem base estão fixados 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

### 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.

Harness opcional de marca d'água em imagens MarkDiffusion

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:

  1. Harness de verificação (como o MarkLLM, mas para imagens): marcar uma imagem de teste com um esquema, executar a remoção e re-detectar com a mesma configuração de esquema — por exemplo, provar que uma marca da classe Tree-Ring é eliminada sob o seu pipeline. É um harness de verificação, não um oráculo: a detecção requer o modelo gerador (e chaves para esquemas baseados em chaves), portanto não pode certificar que um detector de fornecedor falhará numa imagem arbitrária.
  2. Motor opcional de remoção de pixels: o seu ataque de regeneração 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.
  3. Detector local de mesmo esquema para marcas da classe Tree-Ring, preenchendo parcialmente a lacuna "sem detector local para StegaStamp/Tree-Ring/StableSignature" (cobre Tree-Ring/Ring-ID/Gaussian-Shading etc., não StegaStamp / StableSignature / SynthID-media).

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

Bootstrap (PyPI pin default; creates ~/markdiffusion/.venv, installs deps).

"$SCRIPTS/setup_markdiffusion.sh"

1. Generate a Tree-Ring watermarked image (+ unwatermarked control).

echo "a red fox in snow" > /tmp/prompt.txt MARKDIFFUSION_DIR=~/markdiffusion
~/markdiffusion/.venv/bin/python "$SCRIPTS/markdiffusion_harness.py" watermark
/tmp/prompt.txt -o wm.png -o2 plain.png --scheme tr --json

2. Remove with the DiffusionPurification regeneration attack.

MARKDIFFUSION_DIR=~/markdiffusion
~/markdiffusion/.venv/bin/python "$SCRIPTS/markdiffusion_harness.py" purify
wm.png -o wm.purified.png --purification-intensity 0.3 --json

3. Re-detect with the SAME scheme config.

MARKDIFFUSION_DIR=~/markdiffusion
~/markdiffusion/.venv/bin/python "$SCRIPTS/markdiffusion_harness.py" detect
wm.purified.png --scheme tr --detector-type l1_distance --json

Ou 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.

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

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

Por que o qpdf não é suficiente para imagens dentro do PDF

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:

  1. Sem perdas. 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.
  2. Recodificação, apenas com evidência. Qualquer coisa que viva nos próprios segmentos APPn do JPEG — EXIF em APP1, um manifesto C2PA em APP11, recursos do Photoshop em APP13 — viaja com os bytes aos quais está anexada, então o pass-through a preserva. O nível 2 roda a mesma passagem com o pass-through desligado, e apenas quando o nível 1 demonstravelmente deixou algo para trás: um marcador AI/C2PA em qualquer modo, ou, sob 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.

Risco residual após uma limpeza

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):

CanalO que removemosO que pode permanecerVerificação externa (exemplos)
C2PA fortemente vinculado / EXIF / XMPSimMarcas suaves / de pixelsc2patool, Content Credentials verify
Mídia da classe SynthIDRemoção opcional de pixels (CtrlRegen externo); pontuação local caso contrárioMarca d'água de áudio/vídeo; marca d'água residual de pixels após remoçãoFerramentas do fornecedor (por exemplo, Google SynthID / detetor Vertex onde oferecido); pontuador local opcional reverse-SynthID
Texto estatísticoReescrita de melhor esforçoMarcas fortes após edição leveSem detetor universal público; ferramentas do fornecedor quando disponíveis

Contexto da indústria de duas camadas (C2PA + marca d'água impercetível): guia do Institute of AI PM.


Detetores de marca d'água

Verificadores fornecidos pelos fornecedores para verificar se o conteúdo contém marcas de proveniência de IA:

  • Claude: Verificar se um ficheiro foi feito com Claude — lê as credenciais de conteúdo C2PA em imagens, vídeo e áudio para reportar se o Claude esteve envolvido na produção do ficheiro; funciona no navegador. A API de deteção de marca d'água de texto do Claude está atualmente em pré-visualização privada.
  • OpenAI: Verificar conteúdo gerado pela OpenAI — carregue uma imagem ou ficheiro de áudio e verifique sinais de proveniência da OpenAI (metadados C2PA e marcas d'água SynthID). Também está disponível uma API programática.
  • Google DeepMind: SynthID — a tecnologia de marca d'água da Google para imagens, áudio, texto e vídeo gerados por IA, com uma visão geral de como as marcas impercetíveis são incorporadas e detetadas.
  • Gemini: Verificar imagens, vídeos e áudio gerados por IA — o guia da Google para verificar ficheiros na aplicação Gemini usando marcas d'água SynthID e Content Credentials, incluindo limites de carregamento e como ler os resultados.

Opções de remoção (resumo)

OpçãoRemoveNotas
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/metadadosProveniência do ficheiroVer 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 abertosEvitar re-marcação com o modelo de origemAlternativa operacional

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.

Ecossistema

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 — GUI de ambiente de trabalho

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 — interface web no navegador

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 — GUI para macOS

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.

Adicionar um projeto

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.

Hook de pré-commit

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

.pre-commit-config.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

Changelog

v0.7.0 — reescrita da Camada B em /clean, módulo de roubo de marca d'água, remoção de marca d'água de áudio/vídeo e amplitude de benchmark/ferramentas

A 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.
  • Nova tática de reescrita 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).
  • A tática humanize agora aplica a passagem de habilidade de humanização de forma determinística (aspas retas, sem travessões em/en, colapsos de preenchimento, utilizeuse) e nomeia as regras de escrita humana no prompt (#311). rewrite_text.py ganhou um caminho CLI --strategy.
  • Correção da reescrita: tokenização de palavras Unicode na divergência lexical (#305); comparar margens brutas antes do arredondamento e registrar metadados de seleção / p-valores classificados (#249).

Benchmark

  • Busca de receita SynthID + medição robusta (#280); vocabulário de reescrita renomeado, busca entre entradas e ordenação humanize-last (#302); recomendar apenas estratégias que ainda passam após o polimento de humanização (#307).
  • API em massa do Pangram como backend de semelhança humana (#296); benchmark de nível de reescrita mínima robustecido com um corpus de 30 documentos (#257); grade de pesos validada + busca de receita ampliada (#294); corpus de benchmark em polonês (#295).

Roubo de marca d'água

  • Novo módulo de roubo de marca d'água de caixa-preta e downloader de corpus de prompts (#303); limpar estado obsoleto em falha de sonda de reinício (#310).

Áudio / vídeo / imagem

  • Cadeia destrutiva de remoção de marca d'água de áudio para silentcipher/AudioSeal/WavMark (tempo + pitch + EQ + reencode de baixa taxa de bits → M4A) (#266).
  • Purificação de vídeo TrustMark quadro a quadro que colapsa o voto temporal (#265).
  • Caixa uuid de proveniência de conteúdo C2PA reconhecida em MP4/MOV/AVIF/HEIC (#264).
  • Preservar caudas truncadas de MP4 durante a remoção (#242); manter o destino de reencode de áudio distinto do destino de limpeza de contêiner (#278).
  • Ignorar saída descartada do exiftool e SynthID redundante na varredura pós-limpeza (#261); degradar de forma limpa quando o exiftool não consegue processar um PDF (#281).
  • Limitar 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

  • Opção de /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

  • Pontuação de estilometria e alavancas de detector para 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

  • Remover ReDoS polinomial em varreduras de data-URI e JSON-LD (#306); bloquear redirecionamentos HTTP no pontuador SynthID para prevenir SSRF (#252).

CI, ferramentas e documentação

  • CI falha quando requisitos de backend opcionais não podem ser resolvidos (#301); imagem Docker reporta ffmpeg como utilizável e instala Ghostscript (#272); atualizações de dependências (cython #299, scipy #298, ruff #297, docker/setup-buildx-action #237).
  • Documentação: seção Watermark Detectors, referência ao blog "Probing SynthID" do ETH SRI, política de Ecossistema (remover ClaudeWatermarks; exigir que projetos listados usem este repositório) (#292).

v0.6.0 — cobertura de formatos mais ampla, robustez da Camada A, distribuição de plugin e hook, e reescrita guiada por detecção

Cobertura de formato e contêiner

  • AVIF / HEIC: metadados nativos da stdlib e remoção de C2PA (#84, #85)
  • BMP / GIF / TIFF: detecção, inspeção e limpeza de metadados via stdlib — extensões de comentário/XMP de GIF são descartadas enquanto o loop 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)
  • EPUB: limpeza de contêiner via stdlib — metadados OPF e meta/JSON-LD de XHTML limpos, mídia raster/SVG incorporada removida, Camada A aplicada ao texto do corpo XHTML, partes de metadados portadoras de marcadores descartadas e partes criptografadas OCF passadas intactas (#107)
  • XLSX / PPTX / DOCX (OOXML): limpeza nativa via stdlib de metadados de contêiner, texto e mídia incorporada; sempre esvaziar campos de proveniência 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)
  • Contêineres SGML/vetoriais: remoção de metadados em tempo linear para SVG/ODT (GHSA-7vpp-96qp-j9wh) (#147); inspecionar e limpar recursivamente data URIs raster incorporadas em SVGs, HTML e Markdown (#87, #88)
  • Áudio / vídeo: remoção de metadados AI/C2PA para MP4/MOV, WAV e MP3 (#139); detecção e remoção de chunk C2PA de WAV RIFF; suporte a metadados C2PA de FLAC; rejeitar análise parcial de frame ID3v2 (#232); preservar offsets de mídia de MP4 ao remover metadados (#183)
  • PDF: alcançar metadados que vivem dentro de imagens incorporadas e parar de redimensionar o PDF para remover XMP; executar a passagem de imagem profunda independentemente de o exiftool estar instalado; honrar bytes de preenchimento de marcador JPEG e compartilhar um único percorredor de segmentos
  • PNG: detectar nomes de produtos de geradores de IA em metadados de texto PNG; detectar marcadores de IA em texto PNG comprimido (#127); manter a cauda truncada em vez de descartá-la em strips de png/isobmff (#182)

Robustez da Camada A (Unicode invisível)

  • Robustez consolidada da Camada A (#133): remover pontos de código Default_Ignorable reservados sem uso legítimo de intercâmbio (U+2065, U+FFF0U+FFF8, U+E0000, U+E0080U+E00FF, U+E01F0U+E0FFF — reportados como reserved_ignorable), os 66 não-caracteres (U+FDD0U+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 Cf nunca viu (U+180F, U+3164, U+FFA0). 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 embarcada
  • Parar de remover controles de formato de layout visível ao lado de seu próprio script: controles de quadrat de hieróglifos egípcios (U+13430U+1343F), controles de taquigrafia Duployan (U+1BCA0U+1BCA3) e controles musicais de beam/tie/slur/phrase (U+1D173U+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 lugares
  • Polimento de emoji / script: preservar VS16 após emojis isolados fora dos intervalos de bloco; preservar joiners de script, emoji de bandeira e marcas Cf árabes; preservar Unicode multilíngue durante a limpeza de texto (#34)

Reescrita da Camada B e detecção de marca d'água

  • Reescrita iterativa da Camada B guiada por detecção: cada rodada gera variantes --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)
  • Verificação de mesma chave Keyed-Gumbel (Aaronson EXP): novo 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. rewrite_text.py --gumbel-key (env WATERMARKS_GUMBEL_KEY, preferido) o torna o avaliador do loop iterativo (prioridade: gumbel > markllm > divergência lexical) e é exposto como gumbel em /capabilities e /detect. Apenas mesma chave — não é um oráculo de fornecedor; a chave nunca é registrada em log (#190)
  • Benchmarks: benchmark de texto MarkLLM multi-esquema e detecção (#188) e um benchmark reproduzível de remoção de texto SynthID (#145); variantes padrão paraphrase:3; relatório e CSV carregam tentativas por documento (colunas mean_attempts / att, attempts / evaluator / passed); --rewrite-loops espelha --max-loops
  • Detecção: detecção de marca d'água de texto de fornecedor (Gemini SynthID, Claude seam, MarkLLM) mais um sidecar de pontuação de imagem SynthID (#109); novo detector de texto de IA estatístico e estilométrico sem LLM para CI e auditorias (#68, #69)

Distribuição: instalações de plugin, hooks e skill

  • O repositório agora é um plugin do Claude Code e um marketplace de plugin único (.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 CLI
  • install_skill.py ganhou um --target (claude-code, claude-project, cowork, cursor) e um seletor --skill cobrindo ambas as skills distribuídas, além de --list, --link e CLAUDE_CONFIG_DIR. O alvo cowork constrói um pacote de upload reproduzível (dist/<skill>.zip, 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 make: install-claude-code-skill, install-claude-code-text-skill, install-claude-project-skill, package-cowork-skill, package-cowork-text-skill
  • Limpeza automática determinística via hook PostToolUse (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-effort
  • Integração de hook de pre-commit para verificação/limpeza de arquivos em stage (#138); skill de texto leve para Cursor (#35); a descrição de clean-user-facing-text não nomeia mais o Cursor como o único host

Serviço HTTP

  • Endpoints em lote: POST /clean/batch, /inspect/batch (#137) e POST /detect/batch (#151)
  • Preservar extensões de formato de imagem em /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)
  • Rotear formatos binários de sites para seus scanners reais (#177); recusar bombas de DTD/entidade no parser de sitemap (GHSA-pjg6-92pm-mmcf) (#146); um limpador que travou bloqueia o commit em vez de ser lido como limpo (#179); um arquivo de texto ilegível é uma varredura falha, não uma limpa (#169)

Correções de confiabilidade e correção

  • Uma segunda execução de --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 Windows
  • Robustez comportamental: preservar comentários JPEG benignos no modo keep; corrigir a flag engolida de bench-synthid-text; simplificar o passthrough de flags para a sonda Ghostscript e limpar noqa desnecessário de clean_text (lint)

CI / ferramentas / documentação

  • Linting e formatação Ruff com aplicação em CI (#103); adicionar macOS à matriz de testes (#152); adicionar uma configuração CodeRabbit para revisões automatizadas de PR (#222); CODEOWNERS para CODE_OF_CONDUCT/LICENSE e proprietários de revisão principal; atribuir direitos autorais a Guillaume Meyer e colaboradores (#228)
  • Documentação: orientação de reescrita preservando voz e proteção de escolhas de voz/acessibilidade; adições ao Ecossistema (ClaudeWatermarks, unmark-web) e uma nota desencorajando nomes semelhantes; referência arXiv 2402.14904; guia de inicialização automática do Windows via Task Scheduler; base64 portátil em exemplos curl; fixar o motor de texto da skill Cursor embarcada à cópia do serviço (#96)

Não lançado

  • Hook de limpeza de pre-commit (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)
  • Preservação de contêiner OOXML: manter <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)

v0.5.0 — distribuição de serviço e Docker, API HTTP e harnesses de verificação

Distribuição de serviço / Docker

  • Divisão skill/serviço: a skill (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)
  • Serviço HTTP: 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)
  • OpenAPI: 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-validator
  • Imagem Docker principal (service/Dockerfile): serviço de limpeza completo com exiftool / qpdf / c2patool pré-instalados; qualquer CLI permanece executável sobrescrevendo o comando
  • Docker / compose: compose.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)
  • Publicação no GHCR: .github/workflows/release-images.yml publica imagens core, markllm, markdiffusion em tags v*; ctrlregen / synthid nunca são publicadas (licenciamento upstream)
  • Configuração por env: .env.example + guia de configuração do serviço; docker compose carrega .env automaticamente; .env está no gitignore (negar por padrão)
  • Higiene do repositório: .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)
  • Testes: tests/test_http_server.py (13 casos) para o serviço HTTP; todas as suítes redirecionadas para service/scripts/

Harness de marca d'água de imagem MarkDiffusion (opcional)

  • Novo harness 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)
  • Bootstrap 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 / smoke-markdiffusion / docker-markdiffusion-build / docker-markdiffusion-help
  • Testes baseados em mock (tests/test_markdiffusion_harness.py) — sem torch na CI; documento de referência references/markdiffusion.md
  • Documentação: ressalva de verificação apenas de mesmo esquema (não é um oráculo de detector de fornecedor) e ressalva de deriva de regeneração cega no README, SKILL.md, removal-matrix.md, markdiffusion.md

Harness de marca d'água de texto MarkLLM (opcional)

  • Novo harness opcional (checkout externo THU-BPM/MarkLLM, Apache-2.0): detect_text_watermark.py com subcomandos detect / watermark para esquemas KGW e SynthID
  • rewrite_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)
  • Bootstrap setup_markllm.sh + requirements-markllm.txt (deps fixadas) + Dockerfile.markllm e Makefile bootstrap-markllm / smoke-markllm / docker-markllm-build / docker-markllm-help
  • Robustez: carregamento de modelo apenas de cache --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.markllm
  • Testes baseados em mock (tests/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.md

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)

  • Correção do build da imagem markllm: 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.markdiffusion
  • Correção do build da imagem ctrlregen: os pins de pesquisa da era 2023 (safetensors==0.4.3, transformers==4.37.2tokenizers<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)
  • Correção das imagens do harness em tempo de execução: Dockerfile.markllm e Dockerfile.markdiffusion nunca copiavam common.py para /app (bug pré-existente) — adicionado
  • WebP: inspeção e limpeza de metadados apenas com stdlib para chunks RIFF C2PA, XMP, EXIF e perfil ICC (#37)
  • BMP / GIF / TIFF: detecção, inspeção e limpeza de metadados apenas com stdlib — extensões de comentário/XMP do GIF são descartadas enquanto o looping 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
  • EPUB: limpeza de container apenas com stdlib — metadados OPF e meta/JSON-LD do XHTML higienizados, mídia raster/SVG embutida removida, Layer A aplicada ao texto do corpo XHTML, partes de metadados portadoras de marcadores descartadas e partes criptografadas OCF passadas intactas
  • Sanitização de nomes de arquivo: o serviço HTTP recusa nomes de saída inseguros fornecidos pelo cliente
  • Correção do cleaner de frontmatter markdown que travava e vazava chaves de IA aninhadas (#25)
  • Ferramentas de texto recusam entrada binária; --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)
  • Preservar meta tags de gerador CMS com maiúsculas/minúsculas mistas (#42)
  • Preservar invisíveis de script estruturais, remover PUA no Layer A (#38, #52)
  • Preservar joiners de script, emoji de bandeira e marcas Cf árabes no Layer A (#28)
  • Reforçar a auditoria de sites contra SSRF e gzip bombs (#49)
  • SECURITY.md referencia apenas o canal de advisories privados (#51)
  • Windows: portes PowerShell dos bootstraps de setup (#40)
  • Docs: adicionar shields de stars/forks e remover gráfico de star-history; adicionar MarkLLM às referências do README; template de pull request; plano para implantação via Docker CLI + API

v0.4.0 — remoção de pixel, confiança de achados, correções para Windows e falsos positivos

Remoção opcional de pixel CtrlRegen (backend externo)

  • Remoção opcional de marca d'água no domínio de pixel via checkout 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-ctrlregen
  • clean_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 alta
  • Intensidade padrão conservadora 0.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 env
  • O backend nunca é empacotado: noai-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 CtrlRegenEngine diretamente

Confiança de achados e auditorias agregadas

  • Achados agora são classificados como confirmed / probable / informational / likely_false_positive, expostos em JSON de texto/imagem/container e relatórios humanos
  • Novos audit_dir.py (árvore recursiva) e audit_website.py (descoberta de sitemap + crawl) agregam relatórios; documentados em SKILL.md

Correções de falsos positivos

  • DOCX: escanear apenas docProps/customXml, não o corpo visível (#14)
  • Text Layer A: preservar emoji VS16/ZWJ após uma base de emoji; nova flag paranoica --strip-emoji-glue (#22)
  • HTML: tratar tags de gerador CMS como informativas, não metadados de IA (#13)
  • PDF: excluir payloads de stream do scan de bytes de marcadores de IA (#13)
  • Relatórios de inspeção anotam caminhos não suportados/melhor esforço

Suporte a Windows

  • Restringir preexec_fn e os.fchmod exclusivos de POSIX para que gravações e ferramentas opcionais rodem no Windows (#15, #23)
  • Reconfigurar stdio para UTF-8 para que streams redirecionados do Windows não lancem mais exceção com Unicode invisível; perna de CI do Windows + execução de smoke da CLI (#23)

Docs e cadeia de suprimentos

  • Seção CtrlRegen no README + referências de pesquisa (CtrlRegen, UnMarker, ressalva de furtividade forense), aviso de uso responsável; atualizações de SKILL/matrix/vendor-notes/ethics
  • Configuração do Dependabot + CODEOWNERS de caminhos de segurança; atualização de scipy/numpy/opencv-python/scikit-learn/pywavelets e da imagem base para Python 3.14-slim
  • Testes CtrlRegen baseados em mock (sem torch no CI)

v0.3.2 — reforço de segurança (gravações seguras, cliente HTTP, cadeia de suprimentos do CI)

  • Gravações de saída seguras e atômicas: todo cleaner agora grava via arquivo temporário + rename atômico (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ário
  • Reforço do cliente HTTP do rewrite_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_KEY
  • Limites de recursos: entrada máxima padrão 1 GiB → 256 MiB, novo limite de stdin de 64 MiB, orçamento de zip DOCX/ODT 512 MiB → 128 MiB, e RLIMIT_AS/RLIMIT_FSIZE aplicados a subprocessos exiftool/c2patool/SynthID (todos os limites sobrescrevíveis por env)
  • Cadeia de suprimentos: ações de CI fixadas por SHA com permissions: 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
  • Deps do scorer: Pillow atualizado 10.4.0 → 12.3.0 (24 CVEs conhecidas); uso da API verificado contra o commit upstream fixado
  • Testes: 18 novos testes de regressão de segurança (60 no total, todos passando)

v0.3.1 — reescrita mais forte de marca d'água estatística do Layer B

  • A paráfrase padrão do 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
  • Nova --tactic humanize: passo zero-shot "escreva como um humano" visando frases formulaicas de estilo IA
  • Nova --tactic code: reescreve comentários, docstrings e literais de string, e renomeia identificadores locais preservando comportamento e nomes de API públicos
  • O passo estrutural agora emite "prosa humana natural e variada" em vez do típico "estilo profissional claro" de IA
  • Nova --temperature (padrão 0.9) para backends Ollama e OpenAI-compatible
  • Nova --candidates N: gera N reescritas e seleciona a mais lexicalmente divergente (distância Jaccard de bigramas) com uma proteção contra desvio de comprimento
  • Higiene de modelo mais forte: preferir modelos locais de pesos abertos e evitar qualquer fornecedor com marca d'água conhecida, não apenas a origem suspeita
  • O relatório de risco residual agora distingue texto curto/altamente previsível (risco menor) de prosa longa de alta entropia (risco maior)
  • Docs atualizados em SKILL.md, removal-matrix.md e vendor-notes.md; testes cobrem novos prompts, pontuação de divergência e seleção de candidatos

v0.3.0 — pontuação opcional de pixel SynthID

  • Scorer opcional de SynthID no domínio de pixel via checkout externo aloshdenny/reverse-SynthID (score_synthid.py); exposto em inspect_image.py / clean_image.py com REVERSE_SYNTHID_DIR ou --synthid-dir
  • Bootstrap setup_synthid.sh (dependências apenas do scorer; --full instala os requisitos upstream); Dockerfile.synthid mais make docker-synthid-build / docker-synthid-help
  • Alvos smoke-synthid e bootstrap-synthid no Makefile
  • Testes para o adaptador do scorer, caminho de CLI indisponível, parsing de JSON e erros de runtime
  • Docs: apenas detecção/pontuação (sem remoção de pixel); o código upstream não é empacotado e permanece sob sua Research License não comercial

v0.2.0 — correção de falso positivo do c2patool

  • image_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)
  • Novo tests/test_c2patool_report.py (4 casos: sem claim, sem JUMBF, manifesto genuíno, ferramenta ausente)
  • Docs: links de 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 texto

v0.1.0 — polimento de empacotamento + honestidade de proveniência

  • Makefile (test / smoke / install-skill) e pytest.ini
  • Amostras de fixture para Markdown, HTML, SVG; teste de limpeza degradada de PDF
  • Docs: modelo industrial de duas camadas (C2PA de vínculo forte vs vínculo suave / SynthID-media)
  • Tabela de risco residual no README + links para ferramentas externas de verificação
  • Referência: guia C2PA/SynthID do Institute of AI PM
  • Marcas d'água de vínculo suave e de pixel/áudio/vídeo explicitamente fora de escopo em skill/matrix/ethics

v0.0.1 — release inicial multi-fornecedor

  • Skill de agente remove-ai-marks (substitui remove-claude-marks, exclusivo do Claude)
  • Layer A: Unicode invisível / bidi / chars de tag / homoglifos de espaço (inspect_text / clean_text)
  • Layer B: orientação de reescrita + rewrite_text.py opcional (print-prompt, Ollama, OpenAI-compatible)
  • Arquivos: strip de metadados C2PA/IA para PNG, JPEG, SVG, PDF, DOCX, ODT, HTML, Markdown
  • inspect_file.py / clean_file.py unificados
  • Docs multi-fornecedor (Claude, Gemini/classe SynthID, OpenAI, open-LLM)
  • Scripts com stdlib em primeiro lugar; c2patool / exiftool opcionais

License

MIT — veja LICENSE.

Bibliography

Categorias