返回更新列表
新发布Aug 30, 2026

watermarks-remover v0.5.0

一个隐私优先的应用,可从您拥有的内容中去除AI水印。

分享
_ _ _ ____ ___ ____ ____ _  _ ____ ____ _  _ ____    ____ ____ _  _ ____ _  _ ____ ____
| | | |__|  |  |___ |__/ |\/| |__| |__/ |_/  [__  __ |__/ |___ |\/| |  | |  | |___ |__/
|_|_| |  |  |  |___ |  \ |  | |  | |  \ | \_ ___]    |  \ |___ |  | |__|  \/  |___ |  \

watermarks-remover

CI Release Stars Forks

Agent 技能 + 标准库 Python 服务,用于从文本和文件中剥离多厂商 AI 溯源标记——面向你拥有的内容的隐私与卫生需求。该技能是一个轻量客户端:它通过 HTTP 驱动底层机制,因此 agent 主机无需 Python。

层级目标方式
A不可见 Unicode、异体空格、双向文本、标签字符确定性 Python 脚本
B统计式(token 采样)文本水印Agent 重写 + 可选的 rewrite_text.py 钩子
文件C2PA / EXIF / XMP / 文档属性PNG、JPEG、WebP、AVIF、HEIC、BMP、GIF、TIFF、SVG、PDF、DOCX、XLSX、PPTX、EPUB、ODT、HTML、Markdown、MP4/MOV/M4A/M4V、WAV、MP3、FLAC

厂商 / 生态(类级别):ClaudeGemini / SynthID-TextOpenAI 溯源面、open-LLM Kirchenbauer 式(green-list)以及 keyed-Gumbel / EXP(Aaronson)标记。

最新版本: v0.7.0

技能路径:skills/remove-ai-marks/
服务路径:service/
(迁移:原为 remove-claude-marks;斜杠别名 /remove-claude-marks 仍有文档记录)

安装(agent 技能)

该技能不附带任何代码——它通过 HTTP 调用服务。安装该技能(仅 markdown)并启动服务,然后如果服务地址不是 http://127.0.0.1:8765,则设置 WATERMARKS_SERVICE_URL

在 Claude Code 中,最快的途径是内置的 插件市场——无需克隆,且可原地更新。在其他环境中,一个安装器即可覆盖所有受支持的主机 (Python 3.10+ 标准库,无依赖):```bash python3 install_skill.py --skill remove-ai-marks --target claude-code

| Host | Target | Lands in |
| --- | --- | --- |
| Claude Code(个人) | `--target claude-code` | `~/.claude/skills/<skill>`(遵循 `CLAUDE_CONFIG_DIR`) |
| Claude Code(项目) | `--target claude-project --project-dir PATH` | `PATH/.claude/skills/<skill>` |
| Cowork、claude.ai、云会话、routines | `--target cowork` | `dist/<skill>.zip`,在 **Customize → Skills** 下上传 |
| Cursor | `--target cursor`(默认) | `~/.cursor/skills/<skill>` |

随附的技能:`remove-ai-marks`(完整版,由服务支持)和
`clean-user-facing-text`(仅文本,自包含)。`--list` 会打印它们。
除非传入 `--force`,否则现有安装会被保留;替换会先
暂存,并且之前的安装会保留为唯一命名的备份。
`--link` 会为此检出创建符号链接而不是复制,因此编辑会实时
生效。在 Windows 上,使用 `py install_skill.py ...`;`install-skill.sh` 包装器
是为 macOS/Linux shell 提供的。

在写入任何内容之前,安装程序会根据
[Agent Skills](https://agentskills.io) 打包规则验证该技能,这些规则是 claude.ai 上传
和 Skills API 所强制执行的:仅规范 frontmatter(`name`、`description`、
`license`、`compatibility`、`metadata`、`allowed-tools`),一个与目录匹配的、最多 64 个字符的
小写连字符 `name`,一个非空的
`description`,最多 1024 个字符。Cowork 包还必须
符合 30 MB 的上传限制,打包程序会强制执行这一点。

### 通过 hook 自动清理(确定性)

技能是一种指令:模型决定是否调用它,而
模型正是产生标记的东西。**hook** 由 harness
在每次匹配的工具调用时执行,不需要配合。这使得 hook 成为此工作流中
确定性的那一半。

该插件在 `Write|Edit|MultiEdit|NotebookEdit` 上注册一个 `PostToolUse` hook,
它会针对 agent 刚刚写入的文件运行 [`service/scripts/hook_written_file.py`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/service/scripts/hook_written_file.py)。
有两种模式,与默认检查的 pre-commit 约定一致:

| Mode | Behaviour |
| --- | --- |
| `check`(默认) | 报告来源标记,不修改文件。发现结果会发送给模型(exit 2),因此它可以提议清理它们。 |
| `clean` | 就地剥离标记,然后告诉模型磁盘上的文件已更改。 |

从插件的设置中设置模式(`/plugin manage` 中的 **Hook mode**,
hook 会将其读取为 `CLAUDE_PLUGIN_OPTION_HOOK_MODE`),或者使用
环境中的 `WATERMARKS_HOOK_MODE=clean`。hook 命令有意
**不**插值 `${user_config.hook_mode}`:Claude Code 拒绝运行
引用了用户从未打开 `/plugin manage` 设置过的选项的 hook——声明的 `default` 并不能满足它——因此
插值它意味着 hook 在全新安装时会静默地永不运行。检测复用 `audit_lib` 的
`scan_file` / `is_actionable`,因此 hook、pre-commit 门禁和 CI
SARIF 导出对什么算作可操作项保持一致;清理会调用
`clean_file.py`,因此不会重复清理逻辑。`clean` 模式会写入
一个同级临时文件,并且仅在存在真实差异时才替换,因此已经
干净的文件会保留其 mtime,并且不会重新触发文件监视器。

如果没有该插件,请自行在 `~/.claude/settings.json`(或项目的
`.claude/settings.json`)中接入它:```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
          }
        ]
      }
    ]
  }
}

在 Windows 上,将 python3 替换为 py

钩子无法做到的事。 没有任何钩子能在你阅读之前重写助手的聊天消息。Claude Code 的 Stop 钩子以只读方式接收 last_assistant_message,并且对于最终响应没有发送前过滤器——这与本项目已针对 Cursor 规则记录的限制相同。因此,确定性保证覆盖代理写入的文件,以及针对任何即将进入 git 的内容的预提交门禁。仅存在于聊天记录中的文本仍然依赖于技能工作流,而该工作流基于模型指令,因此是尽力而为的。

Claude Code 插件(市场)

该仓库同时也是一个 Claude Code 插件和一个单插件市场.claude-plugin/),因此两个技能都可以通过两条命令安装和更新,无需克隆或脚本:``` /plugin marketplace add guillaumemeyer/watermarks-remover /plugin install watermarks-remover@watermarks-remover

技能随后以命名空间方式加载:`/watermarks-remover:remove-ai-marks` 和
`/watermarks-remover:clean-user-facing-text`(当没有其他内容占用该名称时,裸命令 `/remove-ai-marks` 也可用)。`/plugin marketplace update
watermarks-remover` 会拉取后续版本。通过 CLI 使用 `claude plugin marketplace add …` / `claude plugin install …` 也能达到同样效果,从本地检出安装时,传入路径而非 `owner/repo` 即可。

维护者:`make plugin-validate` 会对两个清单运行 `claude plugin validate . --strict`;`tests/test_plugin_manifest.py` 无需 CLI 即可覆盖相同的文件。

### Claude Code```bash
# Personal — available in all your projects
python3 install_skill.py --skill remove-ai-marks --target claude-code
# or: make install-claude-code-skill

# Project — commit .claude/skills/ to share it with the repo
python3 install_skill.py --skill remove-ai-marks --target claude-project \
  --project-dir /path/to/project
# or: make install-claude-project-skill PROJECT=/path/to/project

Claude Code 无需重启即可加载个人和项目技能;/skills 会列出已加载的技能。使用 /remove-ai-marks 调用,或要求“去除 AI 水印 / C2PA / Claude 标记 / SynthID 类文本”。项目安装也是 cloud sessions 所读取的内容,因为它们会克隆仓库并加载其 .claude/skills/

Cowork(以及 claude.ai、cloud sessions、routines)

Cowork 会话不会读取你机器上的 ~/.claude/skills —— 它们加载 为你的 claude.ai 账户启用的技能,并在会话启动时同步。 因此,在那里通过上传一个 bundle 来安装:```bash python3 install_skill.py --skill remove-ai-marks --target cowork

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

然后,在 Claude Desktop 应用中,打开 **Customize → Skills → Add** 并上传
该 zip 文件(claude.ai 上的相同技能设置也同样适用)。该捆绑包是可复现的,包含一个单一的顶层 `remove-ai-marks/` 目录,
其根目录下有 `SKILL.md`,这正是上传所期望的布局。

服务可达性在这里比在本地安装中更为重要:该技能是一个
轻量级 HTTP 客户端,因此会话必须能够访问 `WATERMARKS_SERVICE_URL`。
在你的机器上本地运行的 Cowork 会话可以访问本地的 `make serve`;
云会话和例程在远程运行,需要一个可从那里访问的服务 URL
(并在其上设置 `WATERMARKS_SERVER_API_KEY`)。如果你想要一个完全
不依赖服务的技能,请改为上传 `clean-user-facing-text` —— 它仅处理文本,
并自带其脚本:```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

### 可选的纯文本技能

[`skills/clean-user-facing-text/`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/skills/clean-user-facing-text) 是一个
自包含技能,适用于已授权的手稿、文档和网页
文案。它排除了图像、C2PA、服务和外部模型工具,并运行
其自带的 Layer A 脚本,而不是调用该服务。```bash
python3 install_skill.py --skill clean-user-facing-text --target claude-code
python3 install_skill.py --skill clean-user-facing-text --target cursor

技能调用由模型选择。在 Cursor 中明确采用此工作流的项目还可以复制可选规则:```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

对于所有项目,请将相同的指令放入 Cursor 的 **User Rules** 中。
规则能提高一致性,但仍然是模型指令;Cursor 并未为最终聊天响应
提供确定性的发送前过滤器。

### 启动服务

最快的路径是本地 HTTP 服务器(仅使用 Python 3.10+ 标准库——无依赖、无 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(无 Docker)

有关在 Windows 登录时自动启动服务(不使用 Docker)的说明,请参阅 docs/windows-autostart.md

有关整个基础设施(核心 + 可选的 harness/重型后端),请参阅下方的 Docker / compose

可选系统工具(存在时自动使用——已预装于核心 Docker 镜像中):

工具作用
c2patool检查 C2PA 清单
exiftool残留元数据剥离(尤其是 PDF
qpdf结构化 PDF 重建——真正的 PDF 剥离所必需(见下文)

核心脚本仅需 Python 3.10+ 标准库。Layer B 模型调用为可选。

快速使用(脚本)```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

### 文本工具拒绝二进制输入

`inspect_text.py`、`clean_text.py` 和 `rewrite_text.py` 处理的是文本。当指向 `.docx`、`.pdf` 或图像文件时,它们过去会解码压缩字节并报告任何出现的码点——这些噪声反映的是压缩过程而非内容——随后 `clean_text.py` 会将这些损坏的字节写回,从而破坏文件。现在它们会拒绝二进制输入,并指出处理该文件的工具:```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.

检测通过魔数加上控制字节比例进行,因此非 UTF-8 编码的文本仍可正常工作。--force-text 可在所有位置覆盖此行为。

无法识别的格式永远不会被自动清理

classify() 将不匹配任何受支持的文本、图像或容器格式的字节标记为 unknown —— 它不再回退为 "text"。在自动模式下,clean_file.py 会拒绝此类文件(退出码 2,不写入任何输出),而不是将其作为 UTF-8 解码并写回被破坏的字节;--as text--force-text 是显式的选择加入方式。inspect_file.py 将文件报告为 unknown(退出码 0),HTTP 服务对 /inspect 返回 kind: "unknown",但会拒绝清理未知格式的 /clean 请求(400 —— 请发送带有已知扩展名的文件名,例如 notes.txt)。

HTTP 服务

同一套机制也可作为标准库 HTTP 服务运行(service/scripts/server.py)—— 这是该技能所使用的接口,也是任何 Web 应用无需内嵌代码即可集成的方式:

方法路径请求体返回
GET/health{"ok": true, "version": ...}
GET/capabilities可用的可选工具 / 后端(每个工具都经过版本探测,而不仅仅是在 PATH 上找到)
GET/openapi.json动态生成的 OpenAPI 3.0.3 规范
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": {...}}{"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", ...}}, ...]}

批量端点循环执行与 /inspect/detect/clean/watermark 相同的单文件流水线,每个请求最多处理 WATERMARKS_MAX_BATCH_FILES 个文件(默认 50)。格式错误的条目(错误的 base64、未知选项、无法识别的格式)会以该条目的 "ok": false"error" 字符串呈现 —— 它绝不会中止批处理的其余部分。```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"}"

服务按文件扩展名和魔数进行路由,因此文本 / 图像 / 容器会被自动检测。设置 `WATERMARKS_SERVER_API_KEY` 可要求每个请求都携带 `Authorization: Bearer <key>`。默认仅绑定回环地址(可用 `--host` 覆盖);适用于受信任的网络。

### 水印检测(`/detect` 以及 `detect_before` / `detect_after`)

检测是与清洗分离的独立步骤——除非你主动要求,服务绝不会调用厂商
API:

- **`POST /detect`** 对文件运行已配置的水印检测器。
  文本 → 厂商检测器 + 文体计量分析;图像 → SynthID 像素评分。
- **`/inspect`** 接受一个可选的 `"detect": true` 标志,会将
  检测器结果追加到文本报告中(并可能翻转 `suspicious`)。
- **`/clean`** 接受 `"detect_before"` / `"detect_after"` 选项,用于
  对输入和清洗后的输出进行评分,这样你就能衡量一次清洗
  实际改变了什么。
- **`/clean`** 在 Layer A 之后运行 Layer B 文本重写**默认开启**(对文本而言
  这是必需步骤)。**`"strategy"`** 选项(一个有序的
  `tactic@intensity` 列表,例如 `"[email protected],[email protected]"`)会覆盖
  策略配置文件中的默认值(见下文)。当某个步骤的重写
  后端/模型未配置时,`/clean` 会返回 400。

文本检测器(见 `/capabilities` → `text_detectors`):

文本检测器(见 `/capabilities` → `text_detectors`):

| 检测器 | 激活方式 | 说明 |
| --- | --- | --- |
| `markllm` | `MARKLLM_DIR`(宿主机检出目录) | 研究工具链(KGW / SynthID 方案),仅限同配置——并非厂商预言机。 |
| `gumbel` | `WATERMARKS_GUMBEL_KEY` | 对带密钥的 Gumbel(Aaronson EXP)方案进行无模型同密钥重放(见 `detect_gumbel.py`),仅依赖标准库——适用于 arbi-serve 等自托管引擎;仅限同密钥,并非厂商预言机。 |
| `claude-text` | —(占位符) | Anthropic 已宣布将推出水印检测 API;该接口将在其发布后启用。 |

图像评分:当设置了 `WATERMARKS_SYNTHID_SCORER_URL` 时,服务
通过 `wr-synthid-score` sidecar(heavy 配置)对图像评分;若配置了
本地 `REVERSE_SYNTHID_DIR`,则直接使用该检出目录。检测是
失败软化的:未配置、超时或出错的检测器会报告
`{"available": false, "error": ...}`,且绝不会阻塞清洗。

### 水印生成(`/watermark` 和 `/watermark/batch`)

为基准评估和往返测试生成带水印的文本。
当设置了 `WATERMARKS_SYNTHID_TEXT_URL` 时,服务会将生成委托给
`wr-synthid-text` sidecar(harness 配置);若配置了本地 `MARKLLM_DIR`,则直接使用
该检出目录。与检测类似,生成也是失败软化的:未配置的生成器
会报告 `{"ok": false, "error": ...}`。

## Docker / compose

已发布的镜像(GHCR):

| 镜像标签 | 内容 | 是否发布? |
| --- | --- | --- |
| `ghcr.io/guillaumemeyer/watermarks-remover:<tag>` / `:latest` | 核心 HTTP 服务 + 所有清洗器 + exiftool / qpdf / c2patool | 是 |
| `…:markllm-<tag>` / `:markllm-latest` | MarkLLM 文本水印工具链(上游 Apache-2.0) | 是 |
| `…:markdiffusion-<tag>` / `:markdiffusion-latest` | MarkDiffusion 图像工具链(上游 Apache-2.0) | 是 |
| `watermarks-remover-ctrlregen:local` | CtrlRegen 像素移除——**从不发布**(`noai-watermark` 未附带 LICENSE) | 仅本地构建 |
| `watermarks-remover-synthid-scorer:local` | reverse-SynthID 评分器——**从不发布**(非商业研究许可证) | 仅本地构建(CLI 评分器 + `heavy` 配置下可选的 `wr-synthid-score` HTTP sidecar) |

构建并运行核心服务:```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

全基础设施启动:```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

compose 栈将核心服务映射到 `127.0.0.1:8765`。持久化服务作为后台守护进程运行(`wr-core` 以及 harness profile 下的 `wr-synthid-text` sidecar)。其余 harness/heavy 服务是一次性 CLI——当你需要验证或像素处理时,使用 `docker compose run --rm <service> …` 调用。

验证正在运行的栈(仅退出码,成功时无输出):```bash
make compose-check        # or: ./compose-check.sh

通过 GET /health 检查 wr-core,并以 --help 运行每个 harness/heavy 服务,要求退出码为 0

配置(docker compose 的环境变量)

文本清洗需要 Layer B 配置 —— Layer B 重写是文本 POST /clean 的必需步骤,因此核心服务需要设置好重写后端,否则文本清洗会返回 HTTP 400。图像/容器元数据清洗开箱即用。对于文本,你必须配置 Layer B 策略依赖项:transformers + roberta-large(用于默认的 mlm 步骤)以及 WATERMARKS_REWRITE_* LLM 配置(用于 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"}"

对于排版依赖不换行空格的语言(法语 `« … »`、`; : ! ?` 前的空格),应传入 `"options": {"normalize_spaces": false}`,这相当于 `clean_text.py --no-normalize-spaces` 的 HTTP 等价形式。不可见载体仍会被移除;仅跳过空格重写。

其余内容均为可选,位于仓库根目录的 `.env` 文件中。`docker compose` **会自动加载 `.env`**,并据此插值 `compose.yaml` 中的 `${VAR}` 引用(若两者均已设置,shell 导出优先于 `.env`)。```bash
cp .env.example .env       # then edit
docker compose up -d       # picks up .env automatically

.env 默认被 gitignore 忽略(默认拒绝)——切勿提交它。对于主机侧 CLI 运行(rewrite_text.py、该技能),将同一文件导出到环境中:```bash set -a; . ./.env; set +a; python3 service/scripts/rewrite_text.py /tmp/x.txt -o /tmp/x.rewritten.txt

| 变量 | 作用范围 | 用途 |
| --- | --- | --- |
| `WATERMARKS_SERVER_API_KEY` | `wr-core`(通过 compose `environment`) | 要求 HTTP API 使用 `Authorization: Bearer <key>` |
| `WATERMARKS_GEMINI_*` | — | 2026 年 8 月移除:Google 在 API 上停用了 SynthID 文本水印(见 `vendor-notes.md`) |
| `WATERMARKS_SYNTHID_SCORER_URL` | `wr-core` | 将 core 指向 `wr-synthid-score` sidecar 以进行 SynthID 图像评分(例如 heavy profile 下的 `http://wr-synthid-score:8766`) |
| `WATERMARKS_SYNTHID_SCORER_API_KEY` | `wr-core` + `wr-synthid-score` | 评分器 sidecar 的共享 bearer 密钥(空 = 无认证) |
| `WATERMARKS_SYNTHID_TEXT_URL` | `wr-core` | 将 core 指向 `wr-synthid-text` sidecar 以进行 SynthID 文本水印(例如 harness profile 下的 `http://wr-synthid-text:8767`) |
| `WATERMARKS_SYNTHID_TEXT_API_KEY` | `wr-core` + `wr-synthid-text` | 文本水印 sidecar 的共享 bearer 密钥(空 = 无认证) |
| `WATERMARKS_SYNTHID_TEXT_TIMEOUT` | `wr-core` | 等待 `wr-synthid-text` sidecar 的秒数(默认 120) |
| `WATERMARKS_MARKLLM_SCHEME` | `text_detectors.py`(主机) | 用于 `/detect` 的 MarkLLM 方案:`kgw`(默认)/ `synthid` |
| `HF_TOKEN` | harness/heavy 服务 | 用于受限模型的 Hugging Face token |
| `WATERMARKS_SERVICE_URL` | 仅客户端(skill / curl) | 服务访问地址;默认 `http://127.0.0.1:8765` |
| `WATERMARKS_REWRITE_BACKEND` | `rewrite_text.py` hook | `print-prompt`(默认)/ `ollama` / `openai-compatible` |
| `WATERMARKS_REWRITE_MODEL` | `rewrite_text.py` hook | 模型名称(例如 `deepseek-v4-flash`) |
| `WATERMARKS_REWRITE_BASE_URL` | `rewrite_text.py` hook | API 基础地址(例如 `https://api.deepseek.com`) |
| `WATERMARKS_REWRITE_API_KEY` | `rewrite_text.py` hook | API 密钥 — 仅通过环境变量,绝不放在 argv 中 |
| `WATERMARKS_REWRITE_ALLOW_REMOTE` | `rewrite_text.py` hook | `1` 表示允许非回环端点 |
| `WATERMARKS_REWRITE_REASONING_EFFORT` | `rewrite_text.py` hook | `none`(默认)/ `low` / `medium` / `high` / `off` |
| `WATERMARKS_CLEAN_STRATEGY_FILE` | `server.py` `/clean` | Layer B 策略配置 JSON 的路径(默认 `config/clean_strategy.json`) |
| `WATERMARKS_GUMBEL_KEY` | `detect_gumbel.py` / `text_detectors.py` | 用于 keyed-Gumbel(EXP)同密钥重放的密钥(例如 `0x…`);优先于 argv — 绝不记录日志 |

**文本清洗需要 Layer B。** `/clean` 在 Layer A 之后始终对文本文件应用默认策略(来自 `config/clean_strategy.json`,`{"default_strategy": "[email protected],[email protected]"}`),除非请求传入自己的 `"strategy"` 选项(一个有序的 `tactic@intensity` 列表)。策略步骤为 `tactic@intensity`;`mlm` 步骤需要 `transformers` + `roberta-large`,任何 LLM 步骤(`paraphrase`、`humanize` 等)都需要 `WATERMARKS_REWRITE_*` 配置。如果所需的后端/模型未配置 — 或没有可用策略 — `/clean` **会以 400 拒绝请求**。配置路径的优先级:`--strategy-config` CLI 标志 > `WATERMARKS_CLEAN_STRATEGY_FILE` 环境变量 > 默认的 `config/clean_strategy.json`。

镜像在 `v*` 标签上通过 [`.github/workflows/release-images.yml`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/.github/workflows/release-images.yml) 自动发布。

## 可选的 SynthID 像素评分

当外部检出 [`aloshdenny/reverse-SynthID`](https://github.com/aloshdenny/reverse-SynthID) 可用时,`inspect_image.py` 和 `clean_image.py` 可以报告像素域 SynthID 置信度分数。该评分器**未捆绑**:它在运行时从你的检出中加载,其代码仍受上游项目的非商业研究许可证约束。

### 选项 1:单命令引导(无需 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 接受 --dir PATH--ref REF--full(安装完整的上游 requirements.txt,这会为上游 VAE 绕过添加 torch/diffusers,而本项目并不使用该绕过)。

在 Windows 上使用 setup_synthid.ps1-Dir-Ref-Full),它会在 .venv\Scripts\ 创建 venv —— 这正是 image_meta.pyos.name == "nt" 时已经查找的布局。

选项 2:本地 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

该镜像在构建时从上游源码本地构建。它不会被发布,因此不会重新分发上游代码。

### 选项 3:HTTP 评分器 sidecar(docker compose)

在 `heavy` profile 下,compose 栈还会将评分器作为 HTTP sidecar(`wr-synthid-score`)运行,这样**已发布的核心服务**就可以在清理前后对图像进行评分,而无需捆绑非商业性的上游代码。将 `wr-core` 指向它并共享一个 bearer key(参见 `.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

然后使用 {"options": {"detect_before": true, "detect_after": true}} 调用 POST /clean,会在报告中返回 synthid_before / synthid_after,而对图像调用 POST /detect 会返回 SynthID 分数。故障软处理: 如果 sidecar 不可用或未配置,报告会携带 {"available": false, "error": ...},且清洗仍会成功。

V4 评分使用来自上游检出目录的 artifacts/spectral_codebook_v4.npz (`220 MB)。这仅用于检测/评分——它不会移除像素 水印。

可选的 CtrlRegen 像素移除

对于像素域图像水印(SynthID 类、StegaStamp、Tree-Ring、 StableSignature),可选的外部后端会运行 CtrlRegen 流水线 (ControlNet + DINOv2 IP-Adapter 可控再生成)。该后端是 mertizci/noai-watermark,是 ICLR 2025 CtrlRegen 方法的维护版重新实现,并带有自动分块功能。

该后端未随附打包,且未附带 LICENSE 文件,因此被视为 保留所有权利:它会在固定提交处被克隆,并在运行时加载。 其研究时期的依赖固定版本(requirements-ctrlregen.txt——例如 transformers==4.37.2diffusers==0.27.2)带有已发布的公告, 并且有意不是最新版本,因此它们只会安装在此脚本创建的 专用 venv 中,绝不会安装到主服务镜像中; setup_ctrlregen.sh 还会在现有检出目录上重新验证固定提交, 而不仅仅是全新克隆。

引导```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

在 Windows 上使用 `setup_ctrlregen.ps1`(与 `-Dir`、`-Ref`、`-Python` 相同的标志);
虚拟环境位于 `.venv\Scripts\`,`clean_image.py` 已经能够解析该路径。
它会探测已发布的 PyTorch wheel 索引,并选择不高于 `nvidia-smi` 所打印的 CUDA 版本且实际存在的最高索引——该数字是*驱动*支持的最大值,而驱动向后兼容,因此报告 13.1 的驱动(没有已发布的 `cu131`)会安装 `cu130`。在计算能力低于 7.5 时,它会强制使用 `cu126`,这是最后一个其 wheel 仍包含 Maxwell/Pascal/Volta 内核的索引。它从该索引同时安装 `torch` **和** `torchvision`,这样依赖安装就无法将它们替换为来自 PyPI 的 CPU 构建,然后在安装后验证 `torch.cuda.is_available()` 为 true——如果检测到 GPU 但 torch 最终仅支持 CPU,脚本会大声警告并以非零状态退出,而不是假装设置成功。

### 来自 `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

操作顺序:先进行元数据剥离,然后进行 CtrlRegen 像素移除,最后在同时设置了 REVERSE_SYNTHID_DIR 时,执行可选的 reverse-SynthID 前后评分。

默认情况下强度是保守的--ctrlregen-intensity 0.25),因为更高的强度会移除更多水印,但也会重新生成更多图像内容。已记录的预设值:0.15 最小 / 0.25 默认 / 0.35 平衡 / 0.5 激进 / 0.7 最大(后端默认值为 0.5)。--ctrlregen-steps 默认为 50(有效去噪步数 ≈ 步数 × 强度)。

图像尺寸(512×512 原生限制)

CtrlRegen 是一个 512×512 的 Stable Diffusion 1.5 ControlNet。后端会针对任意输入处理此问题,因此此处不暴露额外的平铺:

  • ≤512 px: 单次处理 — 中心裁剪/缩放至 512,重新生成,再缩放回原尺寸。
  • >512 px: 自动重叠平铺(512 px 瓦片,192 px 重叠),宽度/高度对齐到 8 的倍数,然后进行余弦混合接缝。
  • 两种路径: 输出会缩放回原始尺寸,并与原始图像进行颜色匹配。

非常大的图像(例如 4K)会产生许多瓦片,因此运行时间会随瓦片数量增加(更慢且占用更高 VRAM)。在实际可行时,请预先缩小大尺寸输入;瓦片大小和重叠在上游是硬编码的,不作为标志暴露。

计算、受限模型和验证

预计需要约 10 GB 的模型下载;强烈建议使用 GPU,CPU 运行会很慢。一些上游模型是受限的,因此请导出 HF_TOKEN(仅通过环境变量 — 绝不要通过 argv)。clean_ctrlregen.py 拒绝自动安装依赖;请先运行 setup_ctrlregen.sh

StegaStamp/Tree-Ring/StableSignature 没有本地检测器,因此唯一的本地信号是 reverse-SynthID 分数(一种替代指标)。在可用时,clean_image.py --remove-pixel ctrlregen 会报告该分数在操作前后的值;官方 Google SynthID 检查仍是最终权威。

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

## 可选的 MarkLLM 文本水印验证

对于**受控实验**,一个可选的外部测试框架封装了
[`THU-BPM/MarkLLM`](https://github.com/THU-BPM/MarkLLM)(Apache-2.0),用于
对测试文本添加水印,并在 Layer B 重写后重新检测——例如证明
KGW(Kirchenbauer,即你的“open-LLM”行)或 SynthID-Text(Gemini 行)标记
在你的重写下消失。它是一个**验证测试框架,而非预言机**:
MarkLLM 检测仅对生成时使用的*相同*方案配置 + 密钥有效,
且它无法证明供应商检测器会失败。

后端**未捆绑**。`setup_markllm.sh` 会在固定
提交处克隆上游仓库,创建 venv,并安装固定依赖(torch + transformers);
评分模型(默认 `facebook/opt-1.3b`,Apache-2.0)在首次运行时从 Hugging
Face 下载。```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

围绕 Layer B 重写的验证:--markllm-scheme 传递给 rewrite_text.py(配合 --markllm-dir),它会记录 MarkLLM 检测的 前后结果以及一个 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

**检测引导的迭代重写:** 层 B 现在会迭代重写,并在某次尝试通过评估后立即停止。每一轮评估会生成 `--candidates` 个变体(默认 **1**,`WATERMARKS_REWRITE_CANDIDATES`),而 `--max-loops` 限制在返回最佳努力变体之前运行的轮数(默认 **1**,`WATERMARKS_REWRITE_LOOPS`)。每个变体是一次重写调用加一次评估,并且当评估器报告某次尝试未被水印标记时,该轮会提前退出——因此提高 `--max-loops` 会重试新变体,直到某次评估通过(一次典型的干净重写只需一次尝试)。评估器按优先级选择:

1. **MarkLLM** —— 当传入 `--markllm-scheme`(以及 `--markllm-dir`)时,进行同配置研究检测。在 MarkLLM 之上为 Google 的 SynthID-text 检测器保留了一个供应商检测器槽位,Google 已于 2026 年 8 月在其 API 中停用该检测器——未来供应商端点可以接入此处。
2. **bigram-Jaccard 词汇差异** —— 当未配置检测器时;没有通过/失败判定,因此每次尝试都会被生成,并选择词汇差异最大的那个(原始行为)。

`--json-stats` 报告评估器、尝试次数、通过/失败以及每次尝试的记录:```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"}
}

一个未配置、超时或出错的检测器会产生一个 "available": false 条目,并附带 error 原因,且绝不会导致重写失败——该尝试只是无法通过,循环会回退到 词汇差异选择。当达到最大尝试次数仍未通过时,会返回水印最少(分数最低)的尝试作为尽力而为的结果,并附上说明。

如果后端未配置或其依赖缺失,重写会继续进行,报告会注明验证不可用。推荐使用 GPU;CPU 运行可行但速度较慢,且模型下载需要几 GB。

加固选项:

  • 适配器上的 --offline(或任何 MarkLLM 运行)仅从 Hugging Face 缓存加载评分模型——零网络出口;若未缓存则快速失败。 自定义远程代码绝不会被执行(transformers 的 trust_remote_code 绝不会启用)。
  • WATERMARKS_MARKLLM_RLIMIT_AS=<bytes>(环境变量,POSIX)对 MarkLLM 检测器子进程施加地址空间 限制。默认关闭,因为 torch/CUDA 通常需要较大的地址空间。
  • 配置文件上限为 1 MiB;上游检出和基础镜像 通过 SHA/摘要固定。

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

### Keyed-Gumbel(Aaronson EXP)同密钥验证

[ARBI 的技术报告](https://arbicity.com/news/ai-text-watermarking-for-self-hosted-ai/) 描述了
keyed-Gumbel(“指数”)文本水印——现已随开源
arbi-serve 引擎(`ARBI_WATERMARK_KEY`)发布——其中采样器的噪声源自
对最后 4 个 token 上下文窗口的带密钥哈希。检测是一种
**无模型重放**:仅从文本重新计算 `u = PRF(Hash(key, window), token)`,
并检验 Gamma 尾部,因此不需要 GPU、模型或 logits。
本仓库将该检测器作为 `detect_gumbel.py` 提供(仅使用标准库;p 值
是整数 Gamma 形状的精确 Poisson 和恒等式):```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

与 MarkLLM 相同的诚实性说明:这是一次同密钥重放——仅对生成时使用的同一密钥、分词器和 PRF 布局有效,阴性结果不能证明任何东西。此处的 HMAC-SHA256 布局是一个可审计的实例化,与任何特定引擎内核都不是位兼容的(关于精确重放需要适配什么,请参见模块文档字符串)。

检测引导重写:rewrite_text.py 传入 --gumbel-key(环境变量:WATERMARKS_GUMBEL_KEY,优先使用),迭代重写循环由同密钥 Gumbel 重放驱动——评估器优先级变为 gumbel > MarkLLM > 词汇差异——并带有 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

密钥永远不会出现在统计信息或日志中。持有其引擎密钥的自托管运营者可以验证重写是否清除了 Gumbel 标记;其他所有人只能将 Layer B 视为尽力而为。

## 可选的 SynthID-text 移除基准测试

[`bench_synthid_text.py`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/service/scripts/bench_synthid_text.py) 衡量 Layer B 重写清除 SynthID-text 类水印的有效程度以及代价。它使用 MarkLLM SynthID 方案生成带水印和不带水印的样本(同配置检测,经健全性检查把关),运行你的重写变体(策略 × 最大重写尝试次数;循环在通过时提前停止)以及对照组(不移除、仅 Layer A、可选的重新打标检查),并写出可分享的 `report.md` /
`results.json` / `results.csv`。完整指南:
[`docs/synthid-text-benchmark.md`](https://github.com/guillaumemeyer/watermarks-remover/blob/main/docs/synthid-text-benchmark.md)。

需要 MarkLLM 检出(`setup_markllm.sh` / `MARKLLM_DIR`)以及一个重写后端。**重写模型是你配置的 LLM** —— 即该技能所使用的同一个 `rewrite_text.py` 后端。MarkLLM 默认的
`facebook/opt-1.3b`(`--markllm-model`)只是水印生成器/检测器;它从不执行重写。通过环境变量或基准测试标志配置重写模型(它们与上方的
[配置表](#configuration-env-vars-for-docker-compose) 对应):

| 环境变量 | 基准测试标志 | 默认值 | 含义 |
| --- | --- | --- | --- |
| `WATERMARKS_REWRITE_BACKEND` | `--rewrite-backend` | `ollama` | `ollama` 或 `openai-compatible` |
| `WATERMARKS_REWRITE_MODEL` | `--rewrite-model` | *(必填)* | 执行重写的 LLM(例如 `llama3.2`、`deepseek-v4-flash`) |
| `WATERMARKS_REWRITE_BASE_URL` | `--rewrite-base-url` | `http://127.0.0.1:11434` | 端点;Ollama 默认使用回环地址 |
| `WATERMARKS_REWRITE_API_KEY` | `--rewrite-api-key` | — | API 密钥(仅在子进程的环境变量中,绝不出现在 argv 中) |
| `WATERMARKS_REWRITE_ALLOW_REMOTE=1` | `--rewrite-allow-remote` | 关闭 | 向非回环端点发送内容时必须启用 |```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

重写时使用非原始模型(不要用生成文本时所用的带水印模型来重写),否则重写可能会重新在输出中打上水印;--restamp-control 可对此进行测量。

可选的 MarkDiffusion 图像水印测试框架

对于图像上的受控实验,一个可选的外部测试框架封装了 THU-BPM/MarkDiffusion(Apache-2.0), 这是一个用于潜在扩散模型的生成式水印工具包(它嵌入标记 ——而非移除标记)。我们将其用于三件事:

  1. 验证测试框架(类似 MarkLLM,但针对图像):用某种方案对测试 图像加水印,运行移除,再用相同的方案配置重新检测 ——例如证明 Tree-Ring 类标记在你的流水线下被清除。它是 验证测试框架,而非预言机:检测需要生成 模型(以及基于密钥方案的密钥),因此它无法证明某个厂商 检测器会在任意图像上失败。
  2. 可选的像素移除引擎:其 DiffusionPurification 再生 攻击以 clean_image.py --remove-pixel diffusion 的形式暴露, 作为 CtrlRegen 的替代方案。它是再生(无 ControlNet 条件),因此比 CtrlRegen 更容易使图像内容漂移——保守的 强度默认值(0.3),仅作为回退/对比,绝非 保证。
  3. 本地同方案检测器,用于 Tree-Ring 类标记,部分填补 了“StegaStamp/Tree-Ring/StableSignature 无本地检测器”的空白(它 覆盖 Tree-Ring/Ring-ID/Gaussian-Shading 等,但不包括 StegaStamp / StableSignature / SynthID-media)。

后端未捆绑setup_markdiffusion.sh 会创建一个 venv 并 从 PyPI 安装 markdiffusion==1.0.2(固定版本),torch 则从 正确的平台索引安装;--checkout 则改为在固定 提交处安装可编辑克隆。Stable Diffusion 模型(默认 huanzi05/stable-diffusion-2-1-base)在首次运行时从 Hugging Face 下载。```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

或在正常镜像流水线中运行净化:```bash
MARKDIFFUSION_DIR=~/markdiffusion \
  ~/markdiffusion/.venv/bin/python "$SCRIPTS/clean_image.py" shot.png \
    -o shot.cleaned.png --remove-pixel diffusion

加固开关与 MarkLLM 测试框架保持一致:--offline 仅从 Hugging Face 缓存加载模型(零网络出口,无远程代码),HF_TOKEN 仅通过环境变量传递(绝不通过 argv),算法配置上限为 1 MiB,子进程获得与 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

镜像会安装 CPU 版 torch;CUDA 用户应在宿主机上运行 `setup_markdiffusion.sh`。首次运行时模型下载仍会访问 HF hub。

## 覆盖矩阵

| 渠道 | Claude | Gemini/SynthID | OpenAI | Open-LLM |
| --- | --- | --- | --- | --- |
| Unicode / 基于编辑的文本 | Layer A | Layer A | Layer A | Layer A |
| **统计采样文本** | Layer B 尽力而为(当 Anthropic 的检测 API 发布时使用 Claude seam) | Layer B 尽力而为(+ MarkLLM 同配置测试框架;Google 于 2026 年 8 月停用厂商检测器) | Layer B(如存在) | Layer B 尽力而为 + 可选 MarkLLM 测试框架 |
| C2PA / 文件元数据 | 是(列出的格式) | 存在时是 | 存在时是 | 存在时是 |
| 像素图像标记 | 超出范围 | 可选 SynthID 评分 + CtrlRegen 移除(外部);可选 MarkDiffusion 同方案检测 + DiffusionPurification 移除(外部) | 超出范围 | 可选 CtrlRegen / MarkDiffusion 移除(外部) |
| 训练后门 | 超出范围 | 超出范围 | 超出范围 | 超出范围 |

详情:[`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)。

---

## 文本标记的工作原理(简述)

现代 LLM 水印通常将信号隐藏在**选择哪些 token**(生成式 / 采样偏差)中,而不仅仅隐藏在不可见字符中。基于编辑的方案会注入 Unicode 或同义词规则。文件方案会附加 **C2PA** 或生成器元数据。

- **Layer A** 移除基于编辑的 Unicode 载体(可测试)。
- **Layer B** 通过大幅重写来攻击采样水印(尽力而为;文献标准攻击,如改写 / 回译)。
- **文件清理器** 从支持的容器中剥离 C2PA/XMP/属性。

在厂商发布公开检测器和密钥之前,**没有任何工具能够诚实地证明**“这无法通过官方检查”。报告必须区分可验证的工作与尽力而为的工作。

Layer B 优先使用**非来源**模型(如果你试图避免重新打标,就不要用 Claude 重写 Claude 文本)。

---

## 免责声明:移除文本水印的代价

文本水印存在于**措辞本身**:信号分散在 token 选择中,因此几乎每个句子都携带一点信号。由此产生两个后果,这也是 Layer B 被诚实地描述为*尽力而为*而非魔法橡皮擦的原因。

1. **移除意味着改写,而非重构。** 打乱段落、更改标题或轻微润色几乎不会移动信号。剥离统计标记需要重写文本的很大一部分——逐句进行,而非逐节进行。

2. **改写会降低文案质量。** 任何重写都会用重写模型的用词替换原始用词,从而削弱语气、风格和精确性。对于生产文案(SEO、营销、客户工作),这种质量下降是真实存在的,并且往往对最关心文字的人显而易见。这就像从顶级模型获取文本,然后让能力较弱的模型从头重写:结果无法超过重写模型的上限。

这就引出了诚实的完整循环问题:

> 如果计划无论如何都要用更便宜的模型重写文本,那为什么一开始要付费使用高级模型?直接用更便宜的模型生成更简单、更便宜,并且产生相同——或更好——的最终结果。

当你特别想要高级模型的**思考和起草**,并接受一次重写以满卫生或隐私要求时,Layer B 才有意义——而不是作为获得无标记文本的廉价途径。

**何时跳过 Layer B:**

- **质量比卫生更重要:** 使用无损路径——Layer A Unicode 清理加上文件元数据清理器——并保留原始文本。
- **无论如何都要重写:** 使用**非来源**模型(用来源模型重写可能会重新打标文本),并记住残余风险仍然存在——没有任何工具能证明厂商检测器会失败。

---

## 文件格式

| 格式 | 检查 | 清理 |
| --- | --- | --- |
| PNG / JPEG / WebP | C2PA 块 / APP11 / RIFF `C2PA`、AI XMP 提示 | 丢弃元数据段 |
| AVIF / HEIC | ISOBMFF `jumb` / XMP `uuid` box | 丢弃 box |
| BMP | 尾部非图像字节(无标准化渠道) | 截断尾部元数据,修正文件大小字段 |
| GIF | 注释 / XMP 应用扩展 | 丢弃注释和 XMP,保留 `NETSCAPE2.0` 循环 |
| TIFF(经典 + BigTIFF) | IFD 标签:XMP、EXIF、GPS、IPTC、MakerNote | 丢弃标签,清零载荷,保留 strip |
| SVG | `<metadata>`、XMP | 剥离块 |
| PDF | 字节/XMP + 可选工具 | 先 **exiftool**,再 **qpdf**,然后 **ghostscript** 处理嵌入图像内的元数据;每个缺失的工具会削弱不同层(文档剥离、结构重写、嵌入图像) |
| DOCX | docProps / customXml | 清理属性,丢弃 customXml |
| EPUB | OPF 元数据、XHTML meta/JSON-LD、嵌入媒体 | 清理 OPF,剥离 XHTML meta,清理媒体 + Layer A(跳过加密部分) |
| ODT | meta.xml | 丢弃生成器 / AI 相关 meta |
| HTML | meta、JSON-LD、data-ai* | 剥离标签/属性 |
| Markdown | YAML frontmatter AI 键 | 丢弃键 + Layer A 正文 |
| MP4 / MOV / M4A / M4V | ISOBMFF `jumb`/`uuid` box(与 AVIF/HEIC 相同机制)+ `moov/udta` 生成器标签 | 丢弃 box |
| WAV | RIFF `C2PA` / `LIST INFO` 块、嵌入的 `id3\x20` 块 | 丢弃块 |
| MP3 | ID3v2 帧(v2.3/v2.4 逐帧;v2.2 整标签) | 丢弃匹配的帧或整个标签 |
| FLAC | ID3v2 `GEOB` 帧中的 C2PA 清单 | 丢弃匹配的帧或整个 ID3v2 标签 |

FLAC 支持涵盖 C2PA 标准化的 ID3v2 载体。原生 FLAC 元数据块、Vorbis 注释和波形域水印保持不变。

#### 为什么 PDF 需要 qpdf,而不仅仅是 exiftool

ExifTool 以**增量方式**写入 PDF。`exiftool -all=` 会追加一个 `%BeginExifToolUpdate` 块,释放 Info 对象并从 trailer 中移除 `/Info`——但原始元数据字节仍逐字保留在文件中,且 exiftool 本身可以用 `-PDF-update:all=` 撤销该编辑。命令退出码为 `0`,查看器不显示元数据,而文件反而*变大*,这就是破绽。

对于溯源剥离工具来说,这是一个静默泄漏,因此 `clean_pdf` 在 exiftool 处理后接着运行 `qpdf --linearize`,它会从对象图重新序列化文档并丢弃现已无引用的对象。未安装 `qpdf` 时清理仍会运行,但会明确说明:```
warning: exiftool PDF edits are incremental — the original metadata bytes
remain recoverable; install qpdf for a structural rewrite

为什么 qpdf 不足以处理 PDF 内部的图像

上述两轮处理都作用于文档层面:Info 字典、XMP 数据包、对象图。两者都不会深入到图像 XObject 内部,因此扫描件或 Photoshop 导出的文件——即整页就是一张大 JPEG 的页面——会保留图像所携带的一切。在一个真实的 Photoshop 导出 PDF 上,一次“成功”的清理后仍会留下 27 个标签,其中包括 IFD0:Software、拍摄时间戳和一个预览缩略图;附加在同一图像上的 C2PA 清单同样会幸存下来。

因此 clean_pdf 增加了第三轮处理 deep_images,由 Ghostscript 的 pdfwrite 驱动。它分两级运行,一旦文件干净就立即停止:

  1. 无损。 启用直通的 pdfwrite 会从对象图重建文档,同时逐字节复制压缩图像数据——通过前后对数据流进行哈希校验来验证。这会清除 PDF 在图像周围包裹的一切。直通覆盖 Ghostscript 为其支持的编解码器,即 JPEG(DCTDecode)和 JPEG2000(JPXDecode);Flate、CCITT 和 LZW 图像会被解码并重新编码,对这些编解码器而言在实践中是无损的,但并非逐字节一致。never 是用于数据流必须原封不动保留的文档的选项。
  2. 仅在存在证据时重新编码。 任何存在于 JPEG 自身 APPn 段中的内容——APP1 中的 EXIF、APP11 中的 C2PA 清单、APP13 中的 Photoshop 资源——都会随其所附着的字节一起移动,因此直通会保留它。第 2 级以关闭直通的方式运行同一轮处理,且仅当第 1 级明显留下了某些东西时才运行:任何模式下的 AI/C2PA 标记,或者,在 always 模式下,任何幸存的 APPn 元数据。APP0(JFIF)和 APP2(ICC)保持不变——前者是结构性的,后者决定颜色如何被读取。像素只花在证据上,绝不花在怀疑上。

deep_images 接受 auto(默认:仅当标记在文档剥离后幸存时才运行第 1 级,然后如果它们在第 1 级后仍幸存则运行第 2 级)、always(对每个 PDF 都运行第 1 级,对相机和编辑器 EXIF 也升级到第 2 级)、lossless(仅第 1 级——绝不重新压缩,并通过通常的 still_has_c2pa / post_findings 字段报告任何幸存的内容)以及 never。无法识别的值会被拒绝,而不是被悄悄当作 auto 处理。报告通过 meta.deep_image_passmeta.images_reencoded 说明运行了哪些级别,而当该轮处理被跳过时,它会指出可以进一步处理的选项:```text deep image pass not needed for AI/C2PA markers; pass deep_images="always" to also clear non-AI EXIF inside images

未安装 Ghostscript 时,清理仍会运行,并说明它无法访问的内容:```text
warning: metadata inside embedded images left in place; install ghostscript
for the deep image pass

像素域水印移除现已作为可选的外部 CtrlRegen 后端提供(见上文);它是一种再生式移除器,而非保证。C2PA 软绑定(内容内水印,可在元数据被剥离后重新关联远程 Content Credentials 清单)仍不在范围内。剥离硬绑定 C2PA 不会清除这些通道。

清理后的残余风险

本工具报告可验证的移除(Unicode 计数、元数据操作)以及尽力而为的 Layer B 重写。它无法保证供应商检测器会失败。

如需自行检查残余信号(可选,外部):

通道我们移除的内容可能残留的内容外部检查(示例)
硬绑定 C2PA / EXIF / XMP软绑定 / 像素标记c2patoolContent Credentials verify
SynthID 类媒体可选像素移除(外部 CtrlRegen);否则为本地评分音频/视频水印;移除后的残余像素水印供应商工具(例如 Google SynthID / Vertex 检测器,如提供);可选的本地 reverse-SynthID 评分器
统计文本尽力而为的重写轻度编辑后的强标记无公开的通用检测器;供应商工具(如可用)

行业双层背景(C2PA + 不可感知水印):Institute of AI PM guide


水印检测器

用于验证内容是否带有 AI 溯源标记的供应商提供的检查器:


移除选项(摘要)

选项移除内容备注
Unicode 清理(Layer A)ZWSP、双向控制符、标签、异体空格……文本的安全默认选项
重写(Layer B)统计 token 标记(尽力而为)技能始终提供;以风格为代价——见免责声明
容器/元数据剥离文件溯源见格式表
CtrlRegen 像素移除(可选)像素域图像标记(SynthID 类、StegaStamp、Tree-Ring、StableSignature)外部后端;计算量大;默认保守强度
DiffusionPurification 像素移除(可选)像素域图像标记(Tree-Ring 类)MarkDiffusion 后端;盲再生(比 CtrlRegen 漂移更多);默认保守强度
开放权重本地模型避免用原始模型重新打标操作性替代方案

矩阵:skills/remove-ai-marks/references/removal-matrix.md

伦理与免责声明

skills/remove-ai-marks/references/ethics.md。用于你自己的内容的隐私与研究——而非学术欺诈或虚假的“人类撰写”声明。

负责任使用: 本项目适用于你拥有或获授权处理的内容。用户必须遵守当地法规并负责任地使用。开发者对用户的潜在滥用不承担任何责任。

生态系统

包装或补充本仓库的第三方项目,仅为便于发现而列出。它们不由本项目维护、认可或支持。 本项目不审查其代码,不为其行为或保证背书,也不对你从本列表安装或运行的任何内容负责。每个项目受其自身许可证、维护者和文档约束——使用前请阅读这些内容。

MetaClean — 桌面 GUI

MetaClean 是一个独立的 MIT 许可 Rust/Tauri 桌面应用(Windows、macOS、Linux),提供打包的原生 GUI,用于拖放式元数据清理,并带有系统托盘和资源管理器集成。它是一个独立的代码库:它不调用本仓库的 Python 服务,其支持的格式和清理保证与本项目不同。详情见其 README。

unmark-web — 浏览器 Web UI

unmark-web 是一个独立的、MIT 许可的静态 Web 客户端。它完全在浏览器中移除文本中的不可见 Unicode 标记并剥离图像中的溯源元数据,并可选择调用本仓库的 HTTP 服务来处理其本地不支持的格式。它是一个独立的代码库,与本项目无关联;其范围和限制见其 README。

DropMarks — macOS GUI

DropMarks 是一个独立的 MIT 许可 macOS SwiftUI 应用。它通过这些 stdlib 脚本的 vendored 快照调用本仓库的 inspect_file.py / clean_file.py(以及可选的 rewrite_text.py)。它是一个独立的代码库,与本项目无关联;其范围和限制见其 README。

添加项目

要在此注册项目,请提交一个 PR,添加简短条目——项目名称、它包装或添加的内容,以及指向其自身仓库的链接。条目应简短且实事求是;不要声称与本项目兼容或获得本项目认可。被列出的项目应基于或集成此仓库——例如,通过调用其服务或复用其检测引擎——而不仅仅是独立解决同一问题。请避免使用以 watermarks-remover 开头或与之高度相似的名称——相似名称会让人难以分辨哪个项目是哪个。

Pre-commit 钩子

CI 门禁已经存在(audit_dir.py 的 SARIF 导出,见覆盖矩阵上下文)——下面的 pre-commit 钩子会更早捕获同类问题,甚至在带标记的文件被提交之前。两者都包装现有的 CLI(audit_dir.py / clean_file.py)——没有单独的检测逻辑。```yaml

.pre-commit-config.yaml

repos:

`watermarks-remover-check` 会使提交失败并列出发现的问题;`watermarks-remover-clean` 是可选启用的,它会就地重写已暂存的文件(退出码为 1,以便你审查差异并重新暂存——这与 `ruff --fix` 等自动修复钩子的约定相同)。当清理器完全无法处理某个文件时——它崩溃了、被终止了,或没有生成报告——`watermarks-remover-clean` 会指出该文件并以退出码 3 退出,因此失败的清理器绝不会被误认为是已经干净的文件。可以手动运行 `python3 service/scripts/check_staged.py <files...>` / `clean_staged.py <files...>`。

## 测试```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

更新日志

v0.7.0/clean Layer B 重写、水印窃取模块、音频/视频水印移除,以及基准测试/工具广度

v0.7.0 将 Layer B 统计标记重写引入 /clean 服务本身,由可配置、经基准测试调优的策略([email protected],[email protected])驱动。同时带来:一个黑盒水印窃取模块、破坏性音频和逐帧视频水印移除、大幅增强的重写基准测试,以及一系列加固、安全和工具修复。

服务中的 Layer B 重写

  • /clean 在 Layer A 之后对文本运行 Layer B 重写。默认值来自 config/clean_strategy.json;每请求的 options.strategy 可覆盖它,当所需后端未配置时 /clean 以 400 拒绝(#315)。配置优先级:--strategy-config > WATERMARKS_CLEAN_STRATEGY_FILE > config/clean_strategy.json
  • 新增 mlm 重写策略:遮蔽一部分内容词并用 roberta-large 填充——一种非自回归的局部编辑,因此输出混合了原始 token 流与掩码语言模型预测(#311)。
  • humanize 策略现在确定性地应用 humanizer-skill 处理(直引号、无 en/em 破折号、填充词折叠、utilizeuse),并在提示中列出人类写作规则(#311)。rewrite_text.py 新增了 --strategy CLI 路径。
  • 重写正确性:词汇分歧中的 Unicode 词分词(#305);在舍入前比较原始边际值,并记录选择元数据 / 排序 p 值(#249)。

基准测试

  • SynthID 配方搜索 + 稳健测量(#280);重命名重写词汇表、跨输入搜索,以及 humanize-last 排序(#302);仅推荐在 humanize 润色后仍能通过检测的策略(#307)。
  • Pangram 批量 API 作为人类相似度后端(#296);用 30 文档语料库加固最小重写级别基准测试(#257);验证权重网格 + 扩展配方搜索(#294);波兰语基准语料库(#295)。

水印窃取

  • 新增黑盒水印窃取模块和提示语料库下载器(#303);在重新开始探测失败时清除陈旧状态(#310)。

音频 / 视频 / 图像

  • 针对 silentcipher/AudioSeal/WavMark 的破坏性音频水印移除链(tempo + pitch + EQ + 低比特率重新编码 → M4A)(#266)。
  • 逐帧 TrustMark 视频净化,瓦解时间投票(#265)。
  • 在 MP4/MOV/AVIF/HEIC 上识别 C2PA 内容来源 uuid box(#264)。
  • 在剥离过程中保留截断的 MP4 尾部(#242);保持音频重新编码目标与容器清理目标不同(#278)。
  • 在清理后扫描中跳过被丢弃的 exiftool 输出和冗余的 SynthID(#261);当 exiftool 无法处理 PDF 时优雅降级(#281)。
  • 将解压后的 PNG zTXt/iTXt 限制为 1 MiB(#308);剥离 SVG XML DOCTYPE/ENTITY 声明(#288);保持 DOCX 二进制成员字节安全(#314);保留 OOXML AppVersion(#289)。

HTTP 服务与 CLI

  • /clean 新增保留特殊空格的选项,与 CLI 保持一致(#274);/inspect 在可疑载荷中暴露明确的证据类别(#277);HTTP 请求日志中的时间戳(#256);将载荷字节传入 HTTP SynthID 评分和 inspect_*,以避免冗余的回读。
  • clean_file.py 新增 -q/--quiet/--only-changed(#254)。

技能、插件与钩子

  • clean-user-facing-text 增加文体计量评分和检测器杠杆(#258);PostToolUse 钩子启动器实现跨平台(#255);pre-commit 钩子将字节完全相同的干净非文本文件视为已更改(#238)。

审计

  • audit_dir.py 扫描路由器跳过的源文件、文档和 i18n 文件(#284);扫描 .ts/.tsx/.jsx/.gd 并统一各格式的空格置信度(#273);audit_website.py --sarif 支持(#194);加固就地备份、干净文件状态、SynthID 判定、截断的 ID3v2 和 zip 路由(#201)。

安全

  • 移除 data-URI 和 JSON-LD 扫描中的多项式 ReDoS(#306);在 SynthID 评分器中阻止 HTTP 重定向以防止 SSRF(#252)。

CI、工具与文档

  • 当可选后端依赖无法解析时 CI 失败(#301);Docker 镜像报告 ffmpeg 可用并安装 Ghostscript(#272);依赖升级(cython #299、scipy #298、ruff #297、docker/setup-buildx-action #237)。
  • 文档:Watermark Detectors 章节、ETH SRI "Probing SynthID" 博客引用、Ecosystem 政策(移除 ClaudeWatermarks;要求列出的项目使用本仓库)(#292)。

v0.6.0 — 更广的格式覆盖、Layer A 加固、插件与钩子分发,以及检测引导的重写

格式与容器覆盖

  • AVIF / HEIC:原生 stdlib 元数据和 C2PA 剥离(#84、#85)
  • BMP / GIF / TIFF:stdlib 检测、检查和元数据清理——GIF 注释/XMP 扩展被丢弃,而 NETSCAPE2.0 循环和其他动画块被保留;TIFF IFD 元数据(XMP/EXIF/GPS/IPTC/MakerNote)被丢弃,载荷清零并保留 strip 偏移,适用于经典 TIFF 和 BigTIFF;BMP 尾部元数据被截断并重写文件大小字段(#107)
  • EPUB:stdlib 容器清理——OPF 元数据和 XHTML meta/JSON-LD 被擦除,嵌入的栅格/SVG 媒体被剥离,Layer A 应用于 XHTML 正文文本,携带标记的元数据部分被丢弃,OCF 加密部分原样通过(#107)
  • XLSX / PPTX / DOCX (OOXML):原生 stdlib 容器元数据、文本和嵌入媒体擦除;始终清空 DOCX docProps 来源字段;在移除 customXml 后修剪悬空关系;对 DOCX/ODT 正文文本运行 Layer A;在 Layer A 擦除前解码 XML 实体(#91、#100、#76、#83、#73、#80、#74、#81、#142)
  • SGML/矢量容器:SVG/ODT 的线性时间元数据剥离(GHSA-7vpp-96qp-j9wh)(#147);递归检查和清理 SVG、HTML 和 Markdown 中嵌入的栅格 data URI(#87、#88)
  • 音频 / 视频:MP4/MOV、WAV 和 MP3 的 AI/C2PA 元数据剥离(#139);WAV RIFF C2PA 块检测与移除;FLAC C2PA 元数据支持;拒绝部分 ID3v2 帧解析(#232);在剥离元数据时保留 MP4 媒体偏移(#183)
  • PDF:触及嵌入图像内部的元数据,并停止为剥离 XMP 而调整 PDF 大小;无论是否安装 exiftool 都运行深度图像处理;遵循 JPEG 标记填充字节并共享一个段遍历器
  • PNG:检测 PNG 文本元数据中的 AI 生成器产品名称;检测压缩 PNG 文本中的 AI 标记(#127);在 png/isobmff 剥离中保留截断尾部而非丢弃(#182)

Layer A(不可见 Unicode)加固

  • 整合的 Layer A 加固(#133):剥离没有合法交换用途的保留 Default_Ignorable 码点(U+2065U+FFF0U+FFF8U+E0000U+E0080U+E00FFU+E01F0U+E0FFF——报告为 reserved_ignorable)、66 个非字符(U+FDD0U+FDEF 加上每个平面的 U+FFFE/U+FFFF——报告为 noncharacter),以及 Cf 通配从未捕获的三个空白渲染 Default_Ignorable 载体(U+180FU+3164U+FFA0)。每个都与其已被覆盖的同类具有相同的上下文内保留,因此部分音节文本不会被破坏,且每个都同时应用于服务引擎和 vendored 轻量技能副本
  • 停止剥离紧邻其自身文字的可见布局格式控制符:埃及象形文字 quadrat 控制符(U+13430U+1343F)、Duployan 速记控制符(U+1BCA0U+1BCA3)以及音乐 beam/tie/slur/phrase 控制符(U+1D173U+1D17A)现在在紧邻其自身文字时被保留,而在无关文本之间浮动时仍被剥离(并标记);--strip-emoji-glue 偏执模式仍会在所有位置剥离它们
  • Emoji / 文字润色:在块范围之外的 emoji 单例后保留 VS16;保留文字连接符、旗帜 emoji 和阿拉伯语 Cf 标记;在文本清理期间保留多语言 Unicode(#34)

Layer B 重写与水印检测

  • 迭代式、检测引导的 Layer B 重写:每轮生成 --candidates 个变体(默认 1,WATERMARKS_REWRITE_CANDIDATES),--max-loops(默认 1,WATERMARKS_REWRITE_LOOPS)限制评估轮数,一旦某次尝试通过检测即停止。评估器优先级:MarkLLM(--markllm-scheme)> bigram-Jaccard 词汇分歧(回退)。rewrite_text.py --json-stats 现在报告 evaluator / max_loops / attempts_made / passed 以及每次尝试的 candidate_scores(#153)
  • Keyed-Gumbel(Aaronson EXP)同密钥验证:新的仅用 stdlib 的 detect_gumbel.py 实现无模型重放测试(u = PRF(Hash(key, window), token);精确 Gamma 尾 p 值;重复窗口遮蔽),无需 GPU、模型或 logits。rewrite_text.py --gumbel-key(环境变量 WATERMARKS_GUMBEL_KEY,优先)使其成为迭代循环评估器(优先级:gumbel > markllm > 词汇分歧),并在 /capabilities/detect 中暴露为 gumbel。仅限同密钥——不是供应商预言机;密钥从不记录日志(#190)
  • 基准测试:多方案 MarkLLM 文本基准测试与检测(#188)以及可复现的 SynthID 文本移除基准测试(#145);默认变体 paraphrase:3;报告和 CSV 携带每文档尝试次数(mean_attempts / attattempts / evaluator / passed 列);--rewrite-loops 镜像 --max-loops
  • 检测:供应商文本水印检测(Gemini SynthID、Claude seam、MarkLLM)加上 SynthID 图像评分器 sidecar(#109);新的零 LLM 统计和文体计量 AI 文本检测器,用于 CI 和审计(#68、#69)

分发:插件、钩子和技能安装

  • 本仓库现在是一个 Claude Code 插件和单插件市场.claude-plugin/plugin.json + marketplace.json),因此两个技能都可通过 /plugin marketplace add guillaumemeyer/watermarks-remover 然后 /plugin install watermarks-remover@watermarks-remover 安装,并就地更新。make plugin-validate 运行 claude plugin validate . --stricttests/test_plugin_manifest.py 无需 CLI 即可检查清单
  • install_skill.py 新增了 --targetclaude-codeclaude-projectcoworkcursor)和覆盖两个随附技能的 --skill 选择器,以及 --list--linkCLAUDE_CONFIG_DIRcowork 目标构建可复现的上传包(dist/<skill>.zip,单一顶层技能目录);每个目标都根据 Agent Skills 打包规则和 30 MB 上传限制进行验证。新增 make 目标:install-claude-code-skillinstall-claude-code-text-skillinstall-claude-project-skillpackage-cowork-skillpackage-cowork-text-skill
  • 通过 PostToolUse 钩子实现确定性自动清理hooks/hooks.json + service/scripts/hook_written_file.py):在 agent 写入文件后,无论模型是否配合,harness 都会运行该钩子。check(默认)向模型报告标记;clean 就地剥离它们并告知模型文件已移动,仅在存在真实差异时替换,因此干净文件保留其 mtime。模式来自插件的 hook_mode 设置或 WATERMARKS_HOOK_MODE;检测复用 audit_lib.scan_file / is_actionable,因此钩子、pre-commit 门禁和 CI SARIF 导出一致。钩子仍无法重写助手的聊天消息——不存在这样的钩子点——因此该路径保持尽力而为
  • Pre-commit 钩子集成,用于暂存文件检查/清理(#138);轻量 Cursor 文本技能(#35);clean-user-facing-text 的描述不再将 Cursor 列为唯一宿主

HTTP 服务

  • 批量端点:POST /clean/batch/inspect/batch(#137)和 POST /detect/batch(#151)
  • /clean 中保留图像格式扩展名,并在 av_meta 中使用安全写入(#150);在 /detect curl 示例中使用可移植 base64(并修复引导脚本中 macOS realpath 可移植性,#185)

审计 / 检查与安全

  • audit_dir.py 新增多 worker 并发和 SARIF 2.1.0 导出(#101、#102)
  • 将网站二进制格式路由到其真实扫描器(#177);在 sitemap 解析器中拒绝 DTD/实体炸弹(GHSA-pjg6-92pm-mmcf)(#146);崩溃的清理器阻止提交,而非被读取为干净(#179);不可读的文本文件是扫描失败,而非干净(#169)

可靠性与正确性修复

  • 第二次 --in-place 运行保留原始 .bak;当后续 zip 成员读取失败时保留已收集的证据(#175);截断的 ISOBMFF 容器仍运行 C2PA 字节扫描回退(#176);区分失败的清理器与已干净的文件(#159、#161);将失败的 c2patool 运行视为不确定而非"无 C2PA"(#156);验证清理选项类型(#111);绝不为文本水印检测自动选择 MPS 设备(#99);macOS 可移植性——SynthID 评分器的纯 --json stdout 和 BSD realpath 探测(#70);修复 _ghostscript_usable 中的 Windows subprocess_creationflags 路径,并阻止子进程在 Windows 上打开控制台窗口
  • 行为加固:保留良性 JPEG 注释的 keep 模式;修复 bench-synthid-text 被吞掉的标志;简化 Ghostscript 探测的标志透传和 clean_text 不需要的 noqa(lint)

CI / 工具 / 文档

  • Ruff linting 和格式化并强制 CI(#103);将 macOS 加入测试矩阵(#152);添加 CodeRabbit 配置用于自动 PR 审查(#222);CODEOWNERS 用于 CODE_OF_CONDUCT/LICENSE 和主审查负责人;版权归属于 Guillaume Meyer 和贡献者(#228)
  • 文档:保留声音的重写指导以及保护声音/无障碍选择;Ecosystem 新增(ClaudeWatermarks、unmark-web)以及劝阻相似名称的说明;arXiv 2402.14904 引用;通过 Task Scheduler 的 Windows 自动启动指南;curl 示例中的可移植 base64;将 vendored Cursor-skill 文本引擎固定到服务副本(#96)

未发布

  • Pre-commit 清理钩子(watermarks-remover-clean / clean_staged.py):使用内容摘要(SHA-256)和主动操作检测,以便磁盘上的干净文件被识别,而无需无限重新暂存(#173)
  • OOXML 容器保留:在 DOCX、XLSX 和 PPTX 元数据清理期间保持 docProps/app.xml 中的 <AppVersion> 完整,以满足 ECMA-376 schema 约束并避免 Microsoft Word/Office "unreadable content" 错误(#283)

v0.5.0 — 服务与 Docker 分发、HTTP API 和验证 harness

服务 / Docker 分发

  • 技能/服务拆分:技能(skills/remove-ai-marks/)现在是一个无代码的 HTTP 远程客户端;所有实现移至 service/scripts/ 并在 server.py 后运行,这是一个 stdlib HTTP 入口点(/health/inspect/clean/capabilities
  • HTTP 服务service/scripts/server.py 通过 JSON/base64 暴露清理流水线;加固镜像 CLI(大小上限、二进制防护、原子写入、默认回环、可选 WATERMARKS_SERVER_API_KEY bearer 认证)
  • OpenAPIGET /openapi.json 提供动态生成的 OpenAPI 3.0.3 规范(由路由表 + 实时配置构建,因此永不偏离真实端点);CI 用 openapi-spec-validator 验证它
  • 核心 Docker 镜像service/Dockerfile):完整清理服务,预装 exiftool / qpdf / c2patool;任何 CLI 都可通过覆盖命令保持可运行
  • Docker / composecompose.yaml 启动整个基础设施(core 始终;markllm / markdiffusionprofile: harness 后;ctrlregen / synthidprofile: heavy 后作为仅本地构建);服务前缀为 wr-;harness/heavy 服务默认 command: ["--help"],因此 docker compose up --profile harness --profile heavy 干净退出(一次性 CLI 用 docker compose run 运行);新增 make compose-check / compose-check.sh 验证运行中的栈(仅退出码)
  • GHCR 发布.github/workflows/release-images.ymlv* 标签上发布 coremarkllmmarkdiffusion 镜像;ctrlregen / synthid 从不发布(上游许可)
  • 环境配置.env.example + 服务配置指南;docker compose 自动加载 .env.env 被 gitignore(默认拒绝)
  • 仓库卫生.gitignoreservice/.dockerignore 现在默认拒绝——只有明确允许的路径才能提交或发送到构建上下文(镜像上下文仅发送 service/scripts/,这正是所有 Dockerfile COPY 的内容)
  • 测试:tests/test_http_server.py(13 个用例)用于 HTTP 服务;所有测试套件重新指向 service/scripts/

MarkDiffusion 图像水印 harness(可选)

  • 新的可选 harness(外部 THU-BPM/MarkDiffusion,Apache-2.0):markdiffusion_harness.py 带有 watermark / detect / purify 子命令,支持九种图像方案(Tree-Ring、Ring-ID、ROBIN、WIND、SFW、Gaussian-Shading、GaussMarker、PRC、SEAL)
  • clean_image.py --remove-pixel diffusion 运行 MarkDiffusion DiffusionPurification 再生攻击作为替代像素移除引擎(保守强度默认 0.3)
  • setup_markdiffusion.sh 引导脚本(PyPI 固定 1.0.2--checkout 在固定提交处可编辑克隆)+ requirements-markdiffusion.txt + Dockerfile.markdiffusion 和 Makefile bootstrap-markdiffusion / smoke-markdiffusion / docker-markdiffusion-build / docker-markdiffusion-help
  • 基于 mock 的测试(tests/test_markdiffusion_harness.py)——CI 中无 torch;references/markdiffusion.md 参考文档
  • 文档:仅同方案验证的注意事项(不是供应商检测器预言机)和盲再生漂移注意事项,见 README、SKILL.md、removal-matrix.mdmarkdiffusion.md

MarkLLM 文本水印 harness(可选)

  • 新的可选 harness(外部 THU-BPM/MarkLLM checkout,Apache-2.0):detect_text_watermark.py 带有 detect / watermark 子命令,支持 KGW 和 SynthID 方案
  • rewrite_text.py --markllm-scheme 在 Layer B 重写前后运行检测,并在 --candidates N>1 时进行每候选检测(环境变量门控;报告 cleared
  • setup_markllm.sh 引导脚本 + requirements-markllm.txt(固定依赖)+ Dockerfile.markllm 和 Makefile bootstrap-markllm / smoke-markllm / docker-markllm-build / docker-markllm-help
  • 加固:--offline 仅缓存模型加载(无 HF 出口、无远程代码)、1 MiB 配置上限、重写子进程上可选 WATERMARKS_MARKLLM_RLIMIT_AS、Dockerfile 中固定 torch,以及 Dockerfile.markllm 中的 clone-SHA 验证
  • 基于 mock 的测试(tests/test_markllm_detect.py,21 个用例)——CI 中无 torch;验证 harness 注意事项(仅同配置,不是供应商检测器预言机)记录在 README、SKILL.md、removal-matrix.mdvendor-notes.md

修复与润色- Layer Brewrite_text.py 现在对 openai-compatible 后端默认发送 reasoning_effort: "none"--reasoning-effort / WATERMARKS_REWRITE_REASONING_EFFORToff 表示省略该参数)。否则像 deepseek-v4-flash 这样的推理模型会在一行改写上耗费约 100 秒的思维链(9,894 对 12 个补全 token)

  • 修复 markllm 镜像构建requirements-markllm.txt 固定了 tokenizers==0.23.1,与 transformers==5.15.0 冲突(后者限制 tokenizers<=0.23.0;而 0.23.0 版本并不存在)——现固定为 tokenizers==0.22.2;torch 移至 CPU wheel 索引(torch==2.13.0.*),使镜像与 Dockerfile.markdiffusion 一样仅支持 CPU
  • 修复 ctrlregen 镜像构建:2023 年代的研究固定版本(safetensors==0.4.3transformers==4.37.2tokenizers<0.19)没有 Python 3.14 的 wheel,因此基础镜像现改为 python:3.11-slim(固定摘要,多架构)
  • 修复运行时 harness 镜像Dockerfile.markllmDockerfile.markdiffusion 从未将 common.py 复制到 /app(既有 bug)——已添加
  • WebP:仅用标准库对 RIFF C2PA、XMP、EXIF 和 ICC 配置文件块进行检查与元数据清理(#37)
  • BMP / GIF / TIFF:仅用标准库进行检测、检查和元数据清理——GIF 注释/XMP 扩展被丢弃,同时保留 NETSCAPE2.0 循环;TIFF IFD 元数据(XMP/EXIF/GPS/IPTC/MakerNote)被丢弃,载荷清零并保留 strip 偏移,同时支持经典 TIFF 和 BigTIFF;BMP 尾部元数据被截断并重写文件大小字段
  • EPUB:仅用标准库进行容器清理——OPF 元数据和 XHTML meta/JSON-LD 被擦除,嵌入的栅格/SVG 媒体被剥离,Layer A 应用于 XHTML 正文文本,携带标记的元数据部分被丢弃,OCF 加密部分原样通过
  • 文件名净化:HTTP 服务拒绝客户端提供的不安全输出名称
  • 修复 markdown frontmatter 清理器在嵌套 AI 键上崩溃和泄漏的问题(#25)
  • 文本工具拒绝二进制输入--force-text 可覆盖(#24)
  • --json 不再抑制残留信号退出码(#30)
  • inspect_file 在输出中打印文件名(#50)
  • 保留混合大小写的 CMS 生成器 meta 标签(#42)
  • 在 Layer A 中保留承重脚本不可见字符,剥离 PUA(#38、#52)
  • 在 Layer A 中保留脚本连接符、旗帜 emoji 和阿拉伯语 Cf 标记(#28)
  • 加固网站审计以防御 SSRF 和 gzip 炸弹(#49)
  • SECURITY.md 仅引用私有安全公告渠道(#51)
  • Windows:设置引导脚本的 PowerShell 移植版(#40)
  • 文档:添加 stars/forks 徽章并移除 star-history 图表;在 README 参考中添加 MarkLLM;拉取请求模板;Docker CLI + API 部署计划

v0.4.0 — 像素移除、发现置信度、Windows 与误报修复

可选的 CtrlRegen 像素移除(外部后端)

  • 通过外部 mertizci/noai-watermark 检出实现可选的像素域水印移除:clean_ctrlregen.py 适配器 + setup_ctrlregen.sh 引导脚本(固定提交、稀疏检出、venv、SHA 校验),以及 Dockerfile.ctrlregenmake bootstrap-ctrlregen / docker-ctrlregen-build / smoke-ctrlregen
  • clean_image.py --remove-pixel ctrlregen 执行元数据剥离 → CtrlRegen 移除 → 可选的 reverse-SynthID 前后评分;inspect_image.py 在 SynthID 评分较高时提示该标志
  • 保守的默认强度 0.25(预设 0.15/0.25/0.35/0.5/0.7);512×512 原生流水线由后端自动分块处理更大图像;torch 子进程获得更高的可通过环境变量覆盖的资源上限
  • 后端从不捆绑:noai-watermark 未附带 LICENSE 文件(视为保留所有权利),其自动安装/重启代码路径通过直接使用 CtrlRegenEngine 绕过

发现置信度与聚合审计

  • 发现结果现在分类为 confirmed / probable / informational / likely_false_positive,在文本/图像/容器 JSON 和人类可读报告中暴露
  • 新增 audit_dir.py(递归目录树)和 audit_website.py(站点地图发现 + 爬取)聚合报告;已在 SKILL.md 中记录

误报修复

  • DOCX:仅扫描 docProps/customXml,不扫描可见正文(#14)
  • 文本 Layer A:在 emoji 基字符后保留 emoji VS16/ZWJ;新增 --strip-emoji-glue 偏执标志(#22)
  • HTML:将 CMS 生成器标签视为信息性,而非 AI 元数据(#13)
  • PDF:从 AI 标记字节扫描中排除流载荷(#13)
  • 检查报告注明不支持/尽力而为的路径

Windows 支持

  • 对仅限 POSIX 的 preexec_fnos.fchmod 加门控,使写入和可选工具可在 Windows 上运行(#15、#23)
  • 将 stdio 重新配置为 UTF-8,使重定向的 Windows 流不再因不可见 Unicode 而报错;Windows CI 分支 + CLI 冒烟运行(#23)

文档与供应链

  • README CtrlRegen 章节 + 研究参考(CtrlRegen、UnMarker、取证隐蔽性注意事项)、负责任使用免责声明;SKILL/matrix/vendor-notes/ethics 更新
  • Dependabot 配置 + 安全路径 CODEOWNERS;升级 scipy/numpy/opencv-python/scikit-learn/pywavelets 及基础镜像至 Python 3.14-slim
  • 基于 Mock 的 CtrlRegen 测试(CI 中无 torch)

v0.3.2 — 安全加固(安全写入、HTTP 客户端、CI 供应链)

  • 安全、原子化输出写入:每个清理器现在通过临时文件 + 原子重命名(safe_write_bytes / safe_write_text)写入,拒绝符号链接目标,并通过同一安全路径创建 .bak 备份——预先放置的符号链接(例如在 /tmp 或下载目录中)不再能将清理写入重定向到任意文件
  • rewrite_text.py HTTP 客户端加固:直接拒绝重定向,因此 Authorization 头中的 API 密钥绝不会被重新发送到未经验证的主机;非回环端点默认拒绝(通过 --allow-remoteWATERMARKS_REWRITE_ALLOW_REMOTE=1 选择加入);仅接受 http(s) 方案;--api-key 已移除——密钥仅通过环境变量 WATERMARKS_REWRITE_API_KEY 提供
  • 资源上限:默认最大输入 1 GiB → 256 MiB,新增 64 MiB stdin 上限,DOCX/ODT zip 预算 512 MiB → 128 MiB,并对 exiftool/c2patool/SynthID 子进程应用 RLIMIT_AS/RLIMIT_FSIZE(所有上限均可通过环境变量覆盖)
  • 供应链:CI actions 固定 SHA 并设置 permissions: contents: read,固定开发依赖(requirements-dev.txt),新增 pip-audit 步骤和 CodeQL 工作流;Docker 镜像现在以非特权用户运行并固定 pip
  • 评分器依赖:Pillow 从 10.4.0 升级到 12.3.0(24 个已知 CVE);API 用法已对照固定的上游提交验证
  • 测试:新增 18 个安全回归测试(共 60 个,全部通过)

v0.3.1 — 更强的 Layer B 统计水印改写

  • rewrite_text.py 默认改写现在执行显式的选词 + 句法攻击(从句顺序、连接词、过渡词、句子边界、功能词),而非通用改写
  • 新增 --tactic humanize:零样本“像人类一样写作”处理,针对公式化的 AI 风格措辞
  • 新增 --tactic code:改写注释、文档字符串和字符串字面量,并重命名局部标识符,同时保留行为和公共 API 名称
  • 结构处理现在生成“自然、多样的人类散文”,而非 AI 典型的“清晰专业风格”
  • 新增 --temperature(默认 0.9),适用于 Ollama 和 OpenAI 兼容后端
  • 新增 --candidates N:生成 N 个改写并选择词汇差异最大的(bigram Jaccard 距离),并带有长度漂移保护
  • 更强的模型卫生:优先使用本地开放权重模型,并避免任何已知水印厂商,而不仅仅是疑似来源
  • 残留风险报告现在区分短/高度可预测文本(较低风险)与长、高熵散文(较高风险)
  • 文档已在 SKILL.mdremoval-matrix.mdvendor-notes.md 中更新;测试覆盖新提示、差异评分和候选选择

v0.3.0 — 可选的 SynthID 像素评分

  • 通过外部 aloshdenny/reverse-SynthID 检出实现可选的像素域 SynthID 评分器(score_synthid.py);在 inspect_image.py / clean_image.py 中通过 REVERSE_SYNTHID_DIR--synthid-dir 暴露
  • setup_synthid.sh 引导脚本(仅评分器依赖;--full 安装上游 requirements);Dockerfile.synthid 以及 make docker-synthid-build / docker-synthid-help
  • Makefile smoke-synthidbootstrap-synthid 目标
  • 针对评分器适配器、CLI 不可用路径、JSON 解析和运行时错误的测试
  • 文档:仅检测/评分(无像素移除);上游代码未捆绑,仍受其非商业研究许可证约束

v0.2.0 — c2patool 误报修复

  • image_meta.pyhas_manifest 不再将 Error: No claim found / No JUMBF data found 标记为清单(运算符优先级 bug:否定标记现在否决所有肯定分支)
  • 新增 tests/test_c2patool_report.py(4 个用例:无 claim、无 JUMBF、真实清单、工具缺失)
  • 文档:修复 c2patool 链接(仓库已迁移至 contentauth/c2pa-rs);添加关于文本水印移除质量代价的免责声明

v0.1.0 — 打包完善 + 来源诚实

  • Makefiletest / smoke / install-skill)和 pytest.ini
  • Markdown、HTML、SVG 的固定样本;PDF 降级清理测试
  • 文档:行业两层模型(硬绑定 C2PA 与软绑定 / SynthID-media)
  • README 残留风险表 + 外部验证工具链接
  • 参考:Institute of AI PM C2PA/SynthID 指南
  • 软绑定和像素/音频/视频水印在 skill/matrix/ethics 中明确不在范围内

v0.0.1 — 初始多厂商版本

  • Agent skill remove-ai-marks(替代仅限 Claude 的 remove-claude-marks
  • Layer A: 不可见 Unicode / bidi / 标签字符 / 空格同形字(inspect_text / clean_text
  • Layer B: 改写指导 + 可选 rewrite_text.py(print-prompt、Ollama、OpenAI 兼容)
  • 文件: 针对 PNG、JPEG、SVG、PDF、DOCX、ODT、HTML、Markdown 的 C2PA/AI 元数据剥离
  • 统一的 inspect_file.py / clean_file.py
  • 多厂商文档(Claude、Gemini/SynthID 类、OpenAI、开放 LLM)
  • 标准库优先脚本;可选 c2patool / exiftool

License

MIT — 见 LICENSE

Bibliography

分类