_ _ _ ____ ___ ____ ____ _ _ ____ ____ _ _ ____ ____ ____ _ _ ____ _ _ ____ ____
| | | |__| | |___ |__/ |\/| |__| |__/ |_/ [__ __ |__/ |___ |\/| | | | | |___ |__/
|_|_| | | | |___ | \ | | | | | \ | \_ ___] | \ |___ | | |__| \/ |___ | \
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 |
厂商 / 生态(类级别):Claude、Gemini / SynthID-Text、OpenAI 溯源面、open-LLM Kirchenbauer 式(green-list)以及 keyed-Gumbel / EXP(Aaronson)标记。
最新版本: v0.7.0
技能路径:skills/remove-ai-marks/
服务路径:service/
(迁移:原为 remove-claude-marks;斜杠别名 /remove-claude-marks 仍有文档记录)
该技能不附带任何代码——它通过 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-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/skills —— 它们加载
为你的 claude.ai 账户启用的技能,并在会话启动时同步。
因此,在那里通过上传一个 bundle 来安装:```bash
python3 install_skill.py --skill remove-ai-marks --target cowork
然后,在 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
mkdir -p .grok/skills ln -sfn "$(pwd)/skills/remove-ai-marks" .grok/skills/remove-ai-marks
mkdir -p ~/.grok/skills ln -sfn "$(pwd)/skills/remove-ai-marks" ~/.grok/skills/remove-ai-marks
### 可选的纯文本技能
[`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)的说明,请参阅 docs/windows-autostart.md。
有关整个基础设施(核心 + 可选的 harness/重型后端),请参阅下方的 Docker / compose。
可选系统工具(存在时自动使用——已预装于核心 Docker 镜像中):
| 工具 |
|---|
核心脚本仅需 Python 3.10+ 标准库。Layer B 模型调用为可选。
SCRIPTS=service/scripts
python3 "$SCRIPTS/inspect_file.py" draft.md python3 "$SCRIPTS/clean_file.py" draft.md -o draft.cleaned.md python3 "$SCRIPTS/clean_file.py" photo.png -o photo.cleaned.png python3 "$SCRIPTS/clean_file.py" notes.docx -o notes.cleaned.docx
python3 "$SCRIPTS/inspect_text.py" draft.md python3 "$SCRIPTS/clean_text.py" draft.md -o draft.cleaned.md --stats
python3 "$SCRIPTS/rewrite_text.py" draft.md --backend print-prompt --tactic paraphrase
python3 "$SCRIPTS/inspect_image.py" shot.png python3 "$SCRIPTS/clean_image.py" shot.png -o shot.cleaned.png
### 文本工具拒绝二进制输入
`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 服务运行(service/scripts/server.py)—— 这是该技能所使用的接口,也是任何 Web 应用无需内嵌代码即可集成的方式:
批量端点循环执行与 /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。
文本清洗需要 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.py 在 os.name == "nt" 时已经查找的布局。
make docker-synthid-build
docker run --rm
--user "$(id -u):$(id -g)"
--read-only --tmpfs /tmp
-v "$(pwd):/data"
watermarks-remover-synthid-scorer /data/shot.png
该镜像在构建时从上游源码本地构建。它不会被发布,因此不会重新分发上游代码。
### 选项 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)。这仅用于检测/评分——它不会移除像素
水印。
对于像素域图像水印(SynthID 类、StegaStamp、Tree-Ring、
StableSignature),可选的外部后端会运行 CtrlRegen 流水线
(ControlNet + DINOv2 IP-Adapter 可控再生成)。该后端是
mertizci/noai-watermark,是 ICLR 2025
CtrlRegen 方法的维护版重新实现,并带有自动分块功能。
该后端未随附打包,且未附带 LICENSE 文件,因此被视为
保留所有权利:它会在固定提交处被克隆,并在运行时加载。
其研究时期的依赖固定版本(requirements-ctrlregen.txt——例如
transformers==4.37.2、diffusers==0.27.2)带有已发布的公告,
并且有意不是最新版本,因此它们只会安装在此脚本创建的
专用 venv 中,绝不会安装到主服务镜像中;
setup_ctrlregen.sh 还会在现有检出目录上重新验证固定提交,
而不仅仅是全新克隆。
SCRIPTS=service/scripts
"$SCRIPTS/setup_ctrlregen.sh"
NOAI_WATERMARK_DIR=~/noai-watermark
~/noai-watermark/.venv/bin/python "$SCRIPTS/clean_ctrlregen.py" shot.png -o shot.ctrlregen.png
在 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(有效去噪步数 ≈ 步数 × 强度)。
CtrlRegen 是一个 512×512 的 Stable Diffusion 1.5 ControlNet。后端会针对任意输入处理此问题,因此此处不暴露额外的平铺:
非常大的图像(例如 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 检查仍是最终权威。
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
通常需要较大的地址空间。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 可对此进行测量。
对于图像上的受控实验,一个可选的外部测试框架封装了
THU-BPM/MarkDiffusion(Apache-2.0),
这是一个用于潜在扩散模型的生成式水印工具包(它嵌入标记
——而非移除标记)。我们将其用于三件事:
DiffusionPurification 再生
攻击以 clean_image.py --remove-pixel diffusion 的形式暴露,
作为 CtrlRegen 的替代方案。它是盲再生(无 ControlNet
条件),因此比 CtrlRegen 更容易使图像内容漂移——保守的
强度默认值(0.3),仅作为回退/对比,绝非
保证。后端未捆绑。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
"$SCRIPTS/setup_markdiffusion.sh"
echo "a red fox in snow" > /tmp/prompt.txt
MARKDIFFUSION_DIR=~/markdiffusion
~/markdiffusion/.venv/bin/python "$SCRIPTS/markdiffusion_harness.py" watermark
/tmp/prompt.txt -o wm.png -o2 plain.png --scheme tr --json
MARKDIFFUSION_DIR=~/markdiffusion
~/markdiffusion/.venv/bin/python "$SCRIPTS/markdiffusion_harness.py" purify
wm.png -o wm.purified.png --purification-intensity 0.3 --json
MARKDIFFUSION_DIR=~/markdiffusion
~/markdiffusion/.venv/bin/python "$SCRIPTS/markdiffusion_harness.py" detect
wm.purified.png --scheme tr --detector-type l1_distance --json
或在正常镜像流水线中运行净化:```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 相同的高资源上限。
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
上述两轮处理都作用于文档层面:Info 字典、XMP 数据包、对象图。两者都不会深入到图像 XObject 内部,因此扫描件或 Photoshop 导出的文件——即整页就是一张大 JPEG 的页面——会保留图像所携带的一切。在一个真实的 Photoshop 导出 PDF 上,一次“成功”的清理后仍会留下 27 个标签,其中包括 IFD0:Software、拍摄时间戳和一个预览缩略图;附加在同一图像上的 C2PA 清单同样会幸存下来。
因此 clean_pdf 增加了第三轮处理 deep_images,由 Ghostscript 的 pdfwrite 驱动。它分两级运行,一旦文件干净就立即停止:
pdfwrite 会从对象图重建文档,同时逐字节复制压缩图像数据——通过前后对数据流进行哈希校验来验证。这会清除 PDF 在图像周围包裹的一切。直通覆盖 Ghostscript 为其支持的编解码器,即 JPEG(DCTDecode)和 JPEG2000(JPXDecode);Flate、CCITT 和 LZW 图像会被解码并重新编码,对这些编解码器而言在实践中是无损的,但并非逐字节一致。never 是用于数据流必须原封不动保留的文档的选项。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_pass 和 meta.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 + 不可感知水印):Institute of AI PM guide。
用于验证内容是否带有 AI 溯源标记的供应商提供的检查器:
矩阵:skills/remove-ai-marks/references/removal-matrix.md。
见 skills/remove-ai-marks/references/ethics.md。用于你自己的内容的隐私与研究——而非学术欺诈或虚假的“人类撰写”声明。
负责任使用: 本项目适用于你拥有或获授权处理的内容。用户必须遵守当地法规并负责任地使用。开发者对用户的潜在滥用不承担任何责任。
包装或补充本仓库的第三方项目,仅为便于发现而列出。它们不由本项目维护、认可或支持。 本项目不审查其代码,不为其行为或保证背书,也不对你从本列表安装或运行的任何内容负责。每个项目受其自身许可证、维护者和文档约束——使用前请阅读这些内容。
MetaClean 是一个独立的 MIT 许可 Rust/Tauri 桌面应用(Windows、macOS、Linux),提供打包的原生 GUI,用于拖放式元数据清理,并带有系统托盘和资源管理器集成。它是一个独立的代码库:它不调用本仓库的 Python 服务,其支持的格式和清理保证与本项目不同。详情见其 README。
unmark-web 是一个独立的、MIT 许可的静态 Web 客户端。它完全在浏览器中移除文本中的不可见 Unicode 标记并剥离图像中的溯源元数据,并可选择调用本仓库的 HTTP 服务来处理其本地不支持的格式。它是一个独立的代码库,与本项目无关联;其范围和限制见其 README。
DropMarks 是一个独立的 MIT 许可 macOS SwiftUI 应用。它通过这些 stdlib 脚本的 vendored 快照调用本仓库的 inspect_file.py / clean_file.py(以及可选的 rewrite_text.py)。它是一个独立的代码库,与本项目无关联;其范围和限制见其 README。
要在此注册项目,请提交一个 PR,添加简短条目——项目名称、它包装或添加的内容,以及指向其自身仓库的链接。条目应简短且实事求是;不要声称与本项目兼容或获得本项目认可。被列出的项目应基于或集成此仓库——例如,通过调用其服务或复用其检测引擎——而不仅仅是独立解决同一问题。请避免使用以 watermarks-remover 开头或与之高度相似的名称——相似名称会让人难以分辨哪个项目是哪个。
CI 门禁已经存在(audit_dir.py 的 SARIF 导出,见覆盖矩阵上下文)——下面的 pre-commit 钩子会更早捕获同类问题,甚至在带标记的文件被提交之前。两者都包装现有的 CLI(audit_dir.py / clean_file.py)——没有单独的检测逻辑。```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
/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 破折号、填充词折叠、utilize→use),并在提示中列出人类写作规则(#311)。rewrite_text.py 新增了 --strategy CLI 路径。基准测试
水印窃取
音频 / 视频 / 图像
uuid box(#264)。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)。安全
CI、工具与文档
格式与容器覆盖
NETSCAPE2.0 循环和其他动画块被保留;TIFF IFD 元数据(XMP/EXIF/GPS/IPTC/MakerNote)被丢弃,载荷清零并保留 strip 偏移,适用于经典 TIFF 和 BigTIFF;BMP 尾部元数据被截断并重写文件大小字段(#107)docProps 来源字段;在移除 customXml 后修剪悬空关系;对 DOCX/ODT 正文文本运行 Layer A;在 Layer A 擦除前解码 XML 实体(#91、#100、#76、#83、#73、#80、#74、#81、#142)Layer A(不可见 Unicode)加固
Default_Ignorable 码点(U+2065、U+FFF0–U+FFF8、U+E0000、U+E0080–U+E00FF、U+E01F0–U+E0FFF——报告为 reserved_ignorable)、66 个非字符(U+FDD0–U+FDEF 加上每个平面的 U+FFFE/U+FFFF——报告为 noncharacter),以及 通配从未捕获的三个空白渲染 Default_Ignorable 载体(、、)。每个都与其已被覆盖的同类具有相同的上下文内保留,因此部分音节文本不会被破坏,且每个都同时应用于服务引擎和 vendored 轻量技能副本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)detect_gumbel.py 实现无模型重放测试(u = PRF(Hash(key, window), token);精确 Gamma 尾 p 值;重复窗口遮蔽),无需 GPU、模型或 logits。rewrite_text.py --gumbel-key(环境变量 ,优先)使其成为迭代循环评估器(优先级:gumbel > markllm > 词汇分歧),并在 和 中暴露为 。仅限同密钥——不是供应商预言机;密钥从不记录日志(#190)分发:插件、钩子和技能安装
.claude-plugin/plugin.json + marketplace.json),因此两个技能都可通过 /plugin marketplace add guillaumemeyer/watermarks-remover 然后 /plugin install watermarks-remover@watermarks-remover 安装,并就地更新。make plugin-validate 运行 claude plugin validate . --strict;tests/test_plugin_manifest.py 无需 CLI 即可检查清单install_skill.py 新增了 --target(claude-code、claude-project、cowork、cursor)和覆盖两个随附技能的 选择器,以及 、 和 。 目标构建可复现的上传包(,单一顶层技能目录);每个目标都根据 Agent Skills 打包规则和 30 MB 上传限制进行验证。新增 目标:、、、、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)可靠性与正确性修复
--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 上打开控制台窗口bench-synthid-text 被吞掉的标志;简化 Ghostscript 探测的标志透传和 clean_text 不需要的 noqa(lint)CI / 工具 / 文档
watermarks-remover-clean / clean_staged.py):使用内容摘要(SHA-256)和主动操作检测,以便磁盘上的干净文件被识别,而无需无限重新暂存(#173)docProps/app.xml 中的 <AppVersion> 完整,以满足 ECMA-376 schema 约束并避免 Microsoft Word/Office "unreadable content" 错误(#283)服务 / Docker 分发
skills/remove-ai-marks/)现在是一个无代码的 HTTP 远程客户端;所有实现移至 service/scripts/ 并在 server.py 后运行,这是一个 stdlib HTTP 入口点(/health、/inspect、/clean、/capabilities)service/scripts/server.py 通过 JSON/base64 暴露清理流水线;加固镜像 CLI(大小上限、二进制防护、原子写入、默认回环、可选 WATERMARKS_SERVER_API_KEY bearer 认证)GET /openapi.json 提供动态生成的 OpenAPI 3.0.3 规范(由路由表 + 实时配置构建,因此永不偏离真实端点);CI 用 openapi-spec-validator 验证它service/Dockerfile):完整清理服务,预装 exiftool / qpdf / c2patool;任何 CLI 都可通过覆盖命令保持可运行MarkDiffusion 图像水印 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 / / / MarkLLM 文本水印 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修复与润色- Layer B:rewrite_text.py 现在对 openai-compatible 后端默认发送 reasoning_effort: "none"(--reasoning-effort / WATERMARKS_REWRITE_REASONING_EFFORT;off 表示省略该参数)。否则像 deepseek-v4-flash 这样的推理模型会在一行改写上耗费约 100 秒的思维链(9,894 对 12 个补全 token)
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 一样仅支持 CPUsafetensors==0.4.3、transformers==4.37.2 → tokenizers<0.19)没有 Python 3.14 的 wheel,因此基础镜像现改为 python:3.11-slim(固定摘要,多架构)Dockerfile.markllm 和 Dockerfile.markdiffusion 从未将 复制到 (既有 bug)——已添加可选的 CtrlRegen 像素移除(外部后端)
mertizci/noai-watermark 检出实现可选的像素域水印移除:clean_ctrlregen.py 适配器 + setup_ctrlregen.sh 引导脚本(固定提交、稀疏检出、venv、SHA 校验),以及 Dockerfile.ctrlregen 和 make bootstrap-ctrlregen / docker-ctrlregen-build / smoke-ctrlregenclean_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 中记录误报修复
docProps/customXml,不扫描可见正文(#14)VS16/ZWJ;新增 --strip-emoji-glue 偏执标志(#22)Windows 支持
preexec_fn 和 os.fchmod 加门控,使写入和可选工具可在 Windows 上运行(#15、#23)文档与供应链
safe_write_bytes / safe_write_text)写入,拒绝符号链接目标,并通过同一安全路径创建 .bak 备份——预先放置的符号链接(例如在 /tmp 或下载目录中)不再能将清理写入重定向到任意文件rewrite_text.py HTTP 客户端加固:直接拒绝重定向,因此 Authorization 头中的 API 密钥绝不会被重新发送到未经验证的主机;非回环端点默认拒绝(通过 --allow-remote 或 WATERMARKS_REWRITE_ALLOW_REMOTE=1 选择加入);仅接受 http(s) 方案;--api-key 已移除——密钥仅通过环境变量 WATERMARKS_REWRITE_API_KEY 提供RLIMIT_AS/RLIMIT_FSIZE(所有上限均可通过环境变量覆盖)rewrite_text.py 默认改写现在执行显式的选词 + 句法攻击(从句顺序、连接词、过渡词、句子边界、功能词),而非通用改写--tactic humanize:零样本“像人类一样写作”处理,针对公式化的 AI 风格措辞--tactic code:改写注释、文档字符串和字符串字面量,并重命名局部标识符,同时保留行为和公共 API 名称--temperature(默认 0.9),适用于 Ollama 和 OpenAI 兼容后端--candidates N:生成 N 个改写并选择词汇差异最大的(bigram Jaccard 距离),并带有长度漂移保护SKILL.md、removal-matrix.md 和 vendor-notes.md 中更新;测试覆盖新提示、差异评分和候选选择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-helpsmoke-synthid 和 bootstrap-synthid 目标image_meta.py:has_manifest 不再将 Error: No claim found / No JUMBF data found 标记为清单(运算符优先级 bug:否定标记现在否决所有肯定分支)tests/test_c2patool_report.py(4 个用例:无 claim、无 JUMBF、真实清单、工具缺失)c2patool 链接(仓库已迁移至 contentauth/c2pa-rs);添加关于文本水印移除质量代价的免责声明Makefile(test / smoke / install-skill)和 pytest.iniremove-ai-marks(替代仅限 Claude 的 remove-claude-marks)inspect_text / clean_text)rewrite_text.py(print-prompt、Ollama、OpenAI 兼容)inspect_file.py / clean_file.pyc2patool / exiftoolMIT — 见 LICENSE。
| 作用 |
|---|
c2patool | 检查 C2PA 清单 |
exiftool | 残留元数据剥离(尤其是 PDF) |
qpdf | 结构化 PDF 重建——真正的 PDF 剥离所必需(见下文) |
| 方法 | 路径 | 请求体 | 返回 |
|---|
| 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", ...}}, ...]} |
| 通道 | 我们移除的内容 | 可能残留的内容 | 外部检查(示例) |
|---|
| 硬绑定 C2PA / EXIF / XMP | 是 | 软绑定 / 像素标记 | c2patool、Content Credentials verify |
| SynthID 类媒体 | 可选像素移除(外部 CtrlRegen);否则为本地评分 | 音频/视频水印;移除后的残余像素水印 | 供应商工具(例如 Google SynthID / Vertex 检测器,如提供);可选的本地 reverse-SynthID 评分器 |
| 统计文本 | 尽力而为的重写 | 轻度编辑后的强标记 | 无公开的通用检测器;供应商工具(如可用) |
| 选项 | 移除内容 | 备注 |
|---|
| Unicode 清理(Layer A) | ZWSP、双向控制符、标签、异体空格…… | 文本的安全默认选项 |
| 重写(Layer B) | 统计 token 标记(尽力而为) | 技能始终提供;以风格为代价——见免责声明 |
| 容器/元数据剥离 | 文件溯源 | 见格式表 |
| CtrlRegen 像素移除(可选) | 像素域图像标记(SynthID 类、StegaStamp、Tree-Ring、StableSignature) | 外部后端;计算量大;默认保守强度 |
| DiffusionPurification 像素移除(可选) | 像素域图像标记(Tree-Ring 类) | MarkDiffusion 后端;盲再生(比 CtrlRegen 漂移更多);默认保守强度 |
| 开放权重本地模型 | 避免用原始模型重新打标 | 操作性替代方案 |
CfU+180FU+3164U+FFA0U+13430–U+1343F)、Duployan 速记控制符(U+1BCA0–U+1BCA3)以及音乐 beam/tie/slur/phrase 控制符(U+1D173–U+1D17A)现在在紧邻其自身文字时被保留,而在无关文本之间浮动时仍被剥离(并标记);--strip-emoji-glue 偏执模式仍会在所有位置剥离它们WATERMARKS_GUMBEL_KEY/capabilities/detectgumbelparaphrase:3;报告和 CSV 携带每文档尝试次数(mean_attempts / att、attempts / evaluator / passed 列);--rewrite-loops 镜像 --max-loops--skill--list--linkCLAUDE_CONFIG_DIRcoworkdist/<skill>.zipmakeinstall-claude-code-skillinstall-claude-code-text-skillinstall-claude-project-skillpackage-cowork-skillpackage-cowork-text-skillPostToolUse 钩子实现确定性自动清理(hooks/hooks.json + service/scripts/hook_written_file.py):在 agent 写入文件后,无论模型是否配合,harness 都会运行该钩子。check(默认)向模型报告标记;clean 就地剥离它们并告知模型文件已移动,仅在存在真实差异时替换,因此干净文件保留其 mtime。模式来自插件的 hook_mode 设置或 WATERMARKS_HOOK_MODE;检测复用 audit_lib.scan_file / is_actionable,因此钩子、pre-commit 门禁和 CI SARIF 导出一致。钩子仍无法重写助手的聊天消息——不存在这样的钩子点——因此该路径保持尽力而为clean-user-facing-text 的描述不再将 Cursor 列为唯一宿主compose.yamlcoremarkllmmarkdiffusionprofile: harnessctrlregensynthidprofile: heavywr-command: ["--help"]docker compose up --profile harness --profile heavydocker compose runmake compose-checkcompose-check.sh.github/workflows/release-images.yml 在 v* 标签上发布 core、markllm、markdiffusion 镜像;ctrlregen / synthid 从不发布(上游许可).env.example + 服务配置指南;docker compose 自动加载 .env;.env 被 gitignore(默认拒绝).gitignore 和 service/.dockerignore 现在默认拒绝——只有明确允许的路径才能提交或发送到构建上下文(镜像上下文仅发送 service/scripts/,这正是所有 Dockerfile COPY 的内容)tests/test_http_server.py(13 个用例)用于 HTTP 服务;所有测试套件重新指向 service/scripts/smoke-markdiffusiondocker-markdiffusion-builddocker-markdiffusion-helptests/test_markdiffusion_harness.py)——CI 中无 torch;references/markdiffusion.md 参考文档removal-matrix.md、markdiffusion.md--offline 仅缓存模型加载(无 HF 出口、无远程代码)、1 MiB 配置上限、重写子进程上可选 WATERMARKS_MARKLLM_RLIMIT_AS、Dockerfile 中固定 torch,以及 Dockerfile.markllm 中的 clone-SHA 验证tests/test_markllm_detect.py,21 个用例)——CI 中无 torch;验证 harness 注意事项(仅同配置,不是供应商检测器预言机)记录在 README、SKILL.md、removal-matrix.md、vendor-notes.mdcommon.py/appC2PA、XMP、EXIF 和 ICC 配置文件块进行检查与元数据清理(#37)NETSCAPE2.0 循环;TIFF IFD 元数据(XMP/EXIF/GPS/IPTC/MakerNote)被丢弃,载荷清零并保留 strip 偏移,同时支持经典 TIFF 和 BigTIFF;BMP 尾部元数据被截断并重写文件大小字段--force-text 可覆盖(#24)--json 不再抑制残留信号退出码(#30)inspect_file 在输出中打印文件名(#50)permissions: contents: readrequirements-dev.txtpip-audit