
watermarks-remover v0.5.0
一个隐私优先的应用,可从您拥有的内容中去除AI水印。
_ _ _ ____ ___ ____ ____ _ _ ____ ____ _ _ ____ ____ ____ _ _ ____ _ _ ____ ____
| | | |__| | |___ |__/ |\/| |__| |__/ |_/ [__ __ |__/ |___ |\/| | | | | |___ |__/
|_|_| | | | |___ | \ | | | | | \ | \_ ___] | \ |___ | | |__| \/ |___ | \
watermarks-remover
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 仍有文档记录)
安装(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 镜像中):
核心脚本仅需 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.py 在 os.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.2、diffusers==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),
这是一个用于潜在扩散模型的生成式水印工具包(它嵌入标记
——而非移除标记)。我们将其用于三件事:
- 验证测试框架(类似 MarkLLM,但针对图像):用某种方案对测试 图像加水印,运行移除,再用相同的方案配置重新检测 ——例如证明 Tree-Ring 类标记在你的流水线下被清除。它是 验证测试框架,而非预言机:检测需要生成 模型(以及基于密钥方案的密钥),因此它无法证明某个厂商 检测器会在任意图像上失败。
- 可选的像素移除引擎:其
DiffusionPurification再生 攻击以clean_image.py --remove-pixel diffusion的形式暴露, 作为 CtrlRegen 的替代方案。它是盲再生(无 ControlNet 条件),因此比 CtrlRegen 更容易使图像内容漂移——保守的 强度默认值(0.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 驱动。它分两级运行,一旦文件干净就立即停止:
- 无损。 启用直通的
pdfwrite会从对象图重建文档,同时逐字节复制压缩图像数据——通过前后对数据流进行哈希校验来验证。这会清除 PDF 在图像周围包裹的一切。直通覆盖 Ghostscript 为其支持的编解码器,即 JPEG(DCTDecode)和 JPEG2000(JPXDecode);Flate、CCITT 和 LZW 图像会被解码并重新编码,对这些编解码器而言在实践中是无损的,但并非逐字节一致。never是用于数据流必须原封不动保留的文档的选项。 - 仅在存在证据时重新编码。 任何存在于 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_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 / EXIF / XMP | 是 | 软绑定 / 像素标记 | c2patool、Content Credentials verify |
| SynthID 类媒体 | 可选像素移除(外部 CtrlRegen);否则为本地评分 | 音频/视频水印;移除后的残余像素水印 | 供应商工具(例如 Google SynthID / Vertex 检测器,如提供);可选的本地 reverse-SynthID 评分器 |
| 统计文本 | 尽力而为的重写 | 轻度编辑后的强标记 | 无公开的通用检测器;供应商工具(如可用) |
行业双层背景(C2PA + 不可感知水印):Institute of AI PM guide。
水印检测器
用于验证内容是否带有 AI 溯源标记的供应商提供的检查器:
- Claude: Check if a file was made with Claude — 读取图像、视频和音频中的 C2PA 内容凭证,以报告 Claude 是否参与了该文件的生成;在浏览器中运行。Claude 的文本水印检测 API 目前处于私密预览阶段。
- OpenAI: Verify OpenAI-generated content — 上传图像或音频文件,检查 OpenAI 溯源信号(C2PA 元数据和 SynthID 水印)。也提供程序化 API。
- Google DeepMind: SynthID — Google 针对 AI 生成的图像、音频、文本和视频的水印技术,概述了不可感知标记的嵌入和检测方式。
- Gemini: Verify AI-generated images, videos, and audio — Google 关于在 Gemini 应用中使用 SynthID 水印和 Content Credentials 验证文件的指南,包括上传限制以及如何解读结果。
移除选项(摘要)
| 选项 | 移除内容 | 备注 |
|---|---|---|
| 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:
- repo: https://github.com/guillaumemeyer/watermarks-remover
rev: v0.5.0 # pin to a tag/commit
hooks:
- id: watermarks-remover-check # fails the commit if marks are found
- id: watermarks-remover-clean # opt-in: cleans staged files in place instead
`watermarks-remover-check` 会使提交失败并列出发现的问题;`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 破折号、填充词折叠、utilize→use),并在提示中列出人类写作规则(#311)。rewrite_text.py新增了--strategyCLI 路径。- 重写正确性:词汇分歧中的 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 内容来源
uuidbox(#264)。 - 在剥离过程中保留截断的 MP4 尾部(#242);保持音频重新编码目标与容器清理目标不同(#278)。
- 在清理后扫描中跳过被丢弃的 exiftool 输出和冗余的 SynthID(#261);当 exiftool 无法处理 PDF 时优雅降级(#281)。
- 将解压后的 PNG
zTXt/iTXt限制为 1 MiB(#308);剥离 SVG XML DOCTYPE/ENTITY 声明(#288);保持 DOCX 二进制成员字节安全(#314);保留 OOXMLAppVersion(#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+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),以及Cf通配从未捕获的三个空白渲染 Default_Ignorable 载体(U+180F、U+3164、U+FFA0)。每个都与其已被覆盖的同类具有相同的上下文内保留,因此部分音节文本不会被破坏,且每个都同时应用于服务引擎和 vendored 轻量技能副本 - 停止剥离紧邻其自身文字的可见布局格式控制符:埃及象形文字 quadrat 控制符(
U+13430–U+1343F)、Duployan 速记控制符(U+1BCA0–U+1BCA3)以及音乐 beam/tie/slur/phrase 控制符(U+1D173–U+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/att、attempts/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 . --strict;tests/test_plugin_manifest.py无需 CLI 即可检查清单 install_skill.py新增了--target(claude-code、claude-project、cowork、cursor)和覆盖两个随附技能的--skill选择器,以及--list、--link和CLAUDE_CONFIG_DIR。cowork目标构建可复现的上传包(dist/<skill>.zip,单一顶层技能目录);每个目标都根据 Agent Skills 打包规则和 30 MB 上传限制进行验证。新增make目标:install-claude-code-skill、install-claude-code-text-skill、install-claude-project-skill、package-cowork-skill、package-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);在/detectcurl 示例中使用可移植 base64(并修复引导脚本中 macOSrealpath可移植性,#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 评分器的纯--jsonstdout 和 BSDrealpath探测(#70);修复_ghostscript_usable中的 Windowssubprocess_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_KEYbearer 认证) - OpenAPI:
GET /openapi.json提供动态生成的 OpenAPI 3.0.3 规范(由路由表 + 实时配置构建,因此永不偏离真实端点);CI 用openapi-spec-validator验证它 - 核心 Docker 镜像(
service/Dockerfile):完整清理服务,预装 exiftool / qpdf / c2patool;任何 CLI 都可通过覆盖命令保持可运行 - Docker / compose:
compose.yaml启动整个基础设施(core始终;markllm/markdiffusion在profile: harness后;ctrlregen/synthid在profile: 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.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/
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运行 MarkDiffusionDiffusionPurification再生攻击作为替代像素移除引擎(保守强度默认 0.3)setup_markdiffusion.sh引导脚本(PyPI 固定1.0.2;--checkout在固定提交处可编辑克隆)+requirements-markdiffusion.txt+Dockerfile.markdiffusion和 Makefilebootstrap-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.md、markdiffusion.md
MarkLLM 文本水印 harness(可选)
- 新的可选 harness(外部
THU-BPM/MarkLLMcheckout,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和 Makefilebootstrap-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.md、vendor-notes.md
修复与润色- 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)
- 修复 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.3、transformers==4.37.2→tokenizers<0.19)没有 Python 3.14 的 wheel,因此基础镜像现改为python:3.11-slim(固定摘要,多架构) - 修复运行时 harness 镜像:
Dockerfile.markllm和Dockerfile.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.ctrlregen和make 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_fn和os.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.pyHTTP 客户端加固:直接拒绝重定向,因此Authorization头中的 API 密钥绝不会被重新发送到未经验证的主机;非回环端点默认拒绝(通过--allow-remote或WATERMARKS_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.md、removal-matrix.md和vendor-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-synthid和bootstrap-synthid目标 - 针对评分器适配器、CLI 不可用路径、JSON 解析和运行时错误的测试
- 文档:仅检测/评分(无像素移除);上游代码未捆绑,仍受其非商业研究许可证约束
v0.2.0 — c2patool 误报修复
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);添加关于文本水印移除质量代价的免责声明
v0.1.0 — 打包完善 + 来源诚实
Makefile(test/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
- How Claude marks AI-generated content(Anthropic)
- Dathathri 等,Scalable watermarking for identifying large language model outputs(SynthID-Text,Nature 2024)
- Google AI for Developers,SynthID safeguards(Gemini API 文档)
- C2PA / c2patool
- Kirchenbauer 等,A Watermark for Large Language Models
- Evseev, D.(Arbitration City),Accurate, Costless, and Invisible AI Text Watermarking for Self-Hosted AI Inference(技术报告,2026 年 8 月)——开源 arbi-serve 引擎中发布的键控 Gumbel 水印,支持精确检验检测和推测解码——PDF
- THU-BPM/MarkLLM(用于评估 LLM 水印算法的统一工具包)
- Pan 等,MarkDiffusion: An Open-Source Toolkit for Generative Watermarking of Latent Diffusion Models(JMLR)——本仓库可选图像水印 harness 所包装的嵌入工具包——代码,文档
- Zhang 等,Watermarks in the Sand: Impossibility of Strong Watermarking for Generative Models(ICML 2024)
- Sander 等,Watermarking Makes Language Models Radioactive——水印在微调后仍然存在,并标记在带水印数据上训练的下游模型
- Pan 等,Can LLM Watermarks Robustly Prevent Unauthorized Knowledge Distillation?——基于水印的来源追溯与知识蒸馏防护
- google-deepmind/synthid-text(研究参考;此处不用于检测)
- aloshdenny/reverse-SynthID(研究参考)
- ETH Zurich SRI,Probing SynthID(关于 SynthID 水印可检测性的研究博客)
- Liu 等,Image Watermarks are Removable Using Controllable Regeneration from Clean Noise(ICLR 2025)——可选 CtrlRegen 后端实现的像素再生方法——代码
- Kassis & Hengartner,UnMarker: A Universal Attack on Defensive Image Watermarking(arXiv:2405.08363;IEEE S&P 2025)——一种通用水印攻击,与 CtrlRegen 在不同指标上进行比较
- Goonatilake & Ateniese,Removing the Watermark Is Not Enough: Forensic Stealth in Generative-AI Watermark Removal(arXiv:2605.09203)——为保守强度默认值提供依据:移除仍可能留下取证痕迹
- mertizci/noai-watermark(用于 SynthID/StableSignature/TreeRing 移除和 AI 元数据剥离的 CLI/Python 工具包)
- 0xROOTPLS/DeSynth(针对 OpenAI/Google 图像的 SynthID 移除)
- Institute of AI PM,AI Content Provenance and Watermarking: The PM's Guide to C2PA and SynthID(两层行业模型:C2PA + 不可感知水印 / 软绑定;SB 942 / EU AI Act 第 50 条背景)