☰
watermarks-remover:用 Python 构建隐私优先的 AI 来源标记清除系统与 TaoToken 统一调用
2026/10/4 13:28:12 网站建设 项目流程

1. 为什么要在本地做 AI 来源标记清除

你可能遇到过这种场景:用 Claude Code 或 Cursor 写完一段文案、一份技术文档,甚至一段 Python 脚本,复制到别的地方时,总感觉"哪里不太对"——文本里混着看不见的零宽字符,图片元数据里带着生成来源,PDF 属性里还留着工具链信息。这些就是所谓的 AI 来源标记,也叫水印。它们不一定影响阅读,但在隐私优先的工作流里,你希望自己产出的内容干净、可控、可审计。

watermarks-remover 就是冲着这个需求来的开源项目。它用 Python 实现了一套分层清除流程:Layer A 做确定性的字符级剥离,专门处理 Unicode 隐形载体;Layer B 做统计性采样水印的重写攻击,针对 token 级概率水印;文件层再剥离 C2PA、EXIF、XMP 这些元数据块。它支持文本、图像、文档、音视频多种格式,还能通过 Claude Code 的 PostToolUse hook 和 Cursor 的规则文件集成到本地开发流里。

这篇文章面向三类人:一是用 AI 辅助写作、希望输出内容不带来源标记的创作者;二是用 Claude Code / Cursor 做本地开发、想把清除动作自动化的工程师;三是想理解水印清除技术边界、不想被"一键去水印"营销话术忽悠的技术读者。我会从环境配置讲到可复制的清除脚本骨架,再到验证动作,最后用 TaoToken 统一 Key 和 API 通道把模型调用串起来。全程本地操作,不涉及任何网络代理类工具。

先说清楚一个前提:Layer B 的重写是 best-effort,不是确定性保证。项目文档自己也写了,在厂商公开检测器和密钥之前,任何工具都无法诚实认证"此内容未能通过官方检查"。所以本文的目标是:在本地完成标记清除流程,并确认输出不含可检测的来源标记——而不是承诺"绕过所有检测"。

2. TaoToken 前置:统一 Key 与 API 通道

在写清除脚本之前,先把模型调用通道理清楚。watermarks-remover 的 Layer B 需要一个重写模型来生成候选变体,Layer A 和文件层清理虽然不依赖模型,但如果你想在清除后做一次语义校验、或者让 Agent 自动判断"这段文本是否还需要再重写一轮",就需要一个稳定的模型入口。

我试过在本地同时维护好几套 Key 的做法,Claude Code 一套、Cursor 一套、脚本里再硬编码一套,结果就是轮换 Key 的时候到处改配置,还容易把 Key 提交到仓库里。TaoToken 的思路是把 Key 和 API 通道统一起来:你拿一个 Key,通过一个 Base URL 访问,模型 ID 按需切换。对本地开发场景来说,这省掉了多套凭证管理的麻烦。

TaoToken 的官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 端点是 https://taotoken.net/api ,注意 API 地址不带 UTM 参数。你需要先去控制台创建 Key,地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,然后在 API Keys 页面生成凭证:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。模型对话调试可以用 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。

这里要强调一个概念:TaoToken 是统一的模型调用通道,不是"中转"意义上的灰色服务。你用它来调用模型做重写、校验、Agent 循环,Key 由你自己管理,请求走标准 API 格式。对于 watermarks-remover 这种需要反复调用模型做迭代重写的场景,统一通道的价值在于:脚本、Claude Code、Cursor 三处用同一个 Key,轮换时只改一个地方。

如果你主要做长期编码和 Agent 类任务,可以了解 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。Claude Code 的接入说明在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude-code-anthropic&utm_campaign=rewrite 。下面进入具体配置。

3. 可复制配置:环境、脚本骨架与 settings 片段

这一节是全文的技术核心,我会给出可以直接复制运行的配置和脚本。先装环境,再配 Key,然后写清除脚本骨架。

3.1 Python 环境与依赖

watermarks-remover 的核心 Layer A 和文件元数据剥离只需要 Python 3.10+ 标准库,可选后端才需要额外依赖。先建虚拟环境:

python3.10 -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install watermarks-remover

如果你要用 Layer B 的重写流程,还需要一个模型客户端。这里用 OpenAI 兼容的调用方式,通过 TaoToken 的 Base URL 访问:

pip install openai

PDF 深层清理需要 Ghostscript,macOS 上用 brew 装,Ubuntu 上用 apt:

# macOS brew install ghostscript # Ubuntu / Debian sudo apt-get update && sudo apt-get install -y ghostscript

3.2 统一 Key 的环境变量配置

不要把 Key 写进脚本。用环境变量,并且加进 .gitignore。创建一个.env文件:

# .env —— 不要提交到仓库 TAOTOKEN_API_KEY=sk-你的Key TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_MODEL_ID=claude-sonnet-4-20250514

然后在.gitignore里加上:

.env .venv/ __pycache__/ *.pyc

3.3 Claude Code 的 settings 片段

Claude Code 通过 settings 文件读取模型通道配置。在项目根目录创建.claude/settings.json,写入以下 JSON:

{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的Key", "ANTHROPIC_MODEL": "claude-sonnet-4-20250514" }, "hooks": { "PostToolUse": [ { "matcher": "Write|Edit|MultiEdit|NotebookEdit", "hooks": [ { "type": "command", "command": "python service/scripts/hook_written_file.py --mode clean" } ] } ] } }

这段配置做了两件事:一是把模型请求指向 TaoToken 的统一通道,二是注册 PostToolUse hook,在 Write、Edit、MultiEdit、NotebookEdit 操作完成后自动对目标文件跑清理脚本。--mode clean表示直接清除,改成--mode check则只报告不修改。

3.4 Cursor 的规则文件配置

Cursor 用.cursorrules做项目级规则。在项目根目录创建该文件,写入:

# .cursorrules 当你在本项目中写入或编辑任何文本、代码、Markdown 文件后, 必须对目标文件运行以下清理命令: python -m watermarks_remover.cli clean --path <目标文件路径> --layer A 如果文件是 PDF 或包含元数据,追加 --strip-metadata 参数。 模型调用统一走环境变量 TAOTOKEN_BASE_URL 和 TAOTOKEN_API_KEY, 不要硬编码任何 Key。

3.5 清除脚本骨架

下面是一个可复制的 Python 脚本骨架,把 Layer A 字符级清除、文件元数据剥离、Layer B 重写调用串起来。保存为clean_pipeline.py:

import os import re import sys import json from pathlib import Path from openai import OpenAI # ---------- 配置 ---------- BASE_URL = os.environ.get("TAOTOKEN_BASE_URL", "https://taotoken.net/api") API_KEY = os.environ.get("TAOTOKEN_API_KEY") MODEL_ID = os.environ.get("TAOTOKEN_MODEL_ID", "claude-sonnet-4-20250514") client = OpenAI(base_url=BASE_URL, api_key=API_KEY) # ---------- Layer A:确定性字符级清除 ---------- # 覆盖 BIDI 控制符、零宽字符、变体选择符 INVISIBLE_PATTERN = re.compile( "[" "\u200b-\u200f" # 零宽空格、零宽非连接符、方向标记 "\u202a-\u202e" # BIDI 嵌入与覆盖 "\u2060-\u2064" # 词连接符、不可见分隔 "\ufeff" # BOM "\ufe00-\ufe0f" # 变体选择符 "]" ) def layer_a_clean(text: str) -> str: """剥离隐形 Unicode 载体,不改变可见字符。""" cleaned = INVISIBLE_PATTERN.sub("", text) return cleaned # ---------- Layer B:统计性水印重写 ---------- REWRITE_PROMPT = """你是一个文本重写器。请在不改变原意的前提下, 用不同的措辞和句式重写下面的文本。只输出重写后的正文,不要解释。 原文: {text} """ def layer_b_rewrite(text: str, max_rounds: int = 3) -> str: """迭代重写,直到通过本地启发式检查或达到最大轮数。""" current = text for i in range(max_rounds): resp = client.chat.completions.create( model=MODEL_ID, messages=[{"role": "user", "content": REWRITE_PROMPT.format(text=current)}], temperature=0.9, ) candidate = resp.choices[0].message.content.strip() if not has_residual_marker(candidate): return candidate current = candidate return current def has_residual_marker(text: str) -> bool: """本地启发式检查:是否还残留隐形字符。""" return bool(INVISIBLE_PATTERN.search(text)) # ---------- 文件层:元数据剥离 ---------- def strip_metadata(path: Path) -> None: """对支持的格式剥离 C2PA / EXIF / XMP 元数据块。""" suffix = path.suffix.lower() if suffix in {".png", ".jpg", ".jpeg", ".webp", ".tiff"}: # 调用 watermarks-remover 的文件层清理器 from watermarks_remover.fileclean import strip_image_metadata strip_image_metadata(path) elif suffix == ".pdf": from watermarks_remover.fileclean import strip_pdf_metadata strip_pdf_metadata(path) # 依赖 Ghostscript else: pass # 文本类文件无需元数据剥离 # ---------- 主流程 ---------- def clean_file(path_str: str, use_layer_b: bool = False) -> dict: path = Path(path_str) raw = path.read_text(encoding="utf-8", errors="ignore") after_a = layer_a_clean(raw) result = {"path": str(path), "layer_a_removed": len(raw) - len(after_a)} if use_layer_b: after_b = layer_b_rewrite(after_a) result["layer_b_applied"] = True final = after_b else: final = after_a path.write_text(final, encoding="utf-8") strip_metadata(path) result["residual_marker"] = has_residual_marker(final) return result if __name__ == "__main__": target = sys.argv[1] use_b = "--layer-b" in sys.argv report = clean_file(target, use_layer_b=use_b) print(json.dumps(report, ensure_ascii=False, indent=2))

这个骨架的关键点:Layer A 用正则一次性剥离所有隐形字符,确定性、可重复;Layer B 通过 TaoToken 统一通道调用模型做重写,每轮重写后用本地启发式检查判断是否还有残留;文件层按后缀分派到对应的元数据剥离函数。运行方式:

python clean_pipeline.py ./draft.md python clean_pipeline.py ./draft.md --layer-b

3.6 参数对照表

参数作用默认值建议
--modecheck 只报告 / clean 直接清除check首次用 check 观察
--layer-b是否启用统计水印重写关闭文本类内容按需开启
--strip-metadata是否剥离文件元数据开启图像/PDF 必开
temperature重写随机性0.9越高越易破坏水印,也越易改风格
max_rounds最大重写轮数3超过 3 轮收益递减

注意:Layer B 的重写会改变原文用词和句式,这是消除统计水印的代价。如果你的内容对措辞精度要求极高,建议只用 Layer A 加元数据剥离,或者把 Layer B 的重写结果人工过一遍。

4. 验证请求与成功结果

清除做完不算完,得验证。验证分三层:字符层、元数据层、模型调用层。

4.1 字符层验证

写一个检查脚本,扫描文件里是否还有隐形字符:

import re from pathlib import Path INVISIBLE = re.compile("[\u200b-\u200f\u202a-\u202e\u2060-\u2064\ufeff\ufe00-\ufe0f]") def verify(path_str: str) -> None: text = Path(path_str).read_text(encoding="utf-8", errors="ignore") hits = INVISIBLE.findall(text) if hits: print(f"发现 {len(hits)} 个隐形字符:{[hex(ord(c)) for c in hits]}") else: print("字符层验证通过:未发现隐形载体") if __name__ == "__main__": import sys verify(sys.argv[1])

运行python verify.py ./draft.md,如果输出"字符层验证通过",说明 Layer A 生效了。

4.2 元数据层验证

对图像和 PDF,用 exiftool 检查元数据是否被剥离:

# 安装 exiftool brew install exiftool # macOS sudo apt-get install -y libimage-exiftool-perl # Ubuntu # 检查图像元数据 exiftool ./output.png | grep -iE "c2pa|xmp|exif|provenance" # 检查 PDF 元数据 exiftool ./output.pdf | grep -iE "producer|creator|xmp"

如果 grep 没有输出,说明相关元数据块已被剥离。注意:C2PA 的软绑定模式(仅嵌入链接而非加密签名)不在清除范围内,这类情况 exiftool 可能仍能看到链接字段,但内容本身已无签名。

4.3 模型调用层验证

验证 TaoToken 通道是否正常工作,用一段最小请求:

import os from openai import OpenAI client = OpenAI( base_url=os.environ["TAOTOKEN_BASE_URL"], api_key=os.environ["TAOTOKEN_API_KEY"], ) resp = client.chat.completions.create( model=os.environ.get("TAOTOKEN_MODEL_ID", "claude-sonnet-4-20250514"), messages=[{"role": "user", "content": "回复两个字:正常"}], ) print(resp.choices[0].message.content)

预期输出是"正常"。如果这一步报错,先排查 Key 和 Base URL,再排查模型 ID 是否拼写正确。你也可以在模型对话页面 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 直接发一条消息,确认通道可用。

4.4 端到端成功结果

把三层验证串起来跑一遍,成功的结果长这样:

{ "path": "./draft.md", "layer_a_removed": 47, "layer_b_applied": true, "residual_marker": false }

layer_a_removed是剥离的隐形字符数,residual_marker为 false 表示本地启发式检查通过。再跑一次verify.py,输出"字符层验证通过"。这时候你的文件在字符层和元数据层都是干净的,Layer B 的重写也把统计特征改过了。

需要再次强调:这不等于"通过了厂商官方检测器"。厂商的检测器多为私有实现,密钥未公开,任何工具都无法在本地复现官方判定。你能确认的是:本地可检测的来源标记已清除,输出内容不含隐形载体和已知元数据块。

5. 本篇常见错排查

这一节按真实报错来写,每个都给出定位思路和修复动作。

5.1 401 Unauthorized

报错长这样:

openai.AuthenticationError: Error code: 401 - {'error': {'message': 'Invalid API key'}}

原因通常是三种:Key 没设进环境变量、Key 复制时带了空格、或者 Base URL 写成了带路径的形式。排查顺序:先echo $TAOTOKEN_API_KEY确认变量存在且无多余空格;再确认TAOTOKEN_BASE_URL是https://taotoken.net/api,不要在后面加/v1或斜杠;最后去 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 重新生成一个 Key 试试。如果 Claude Code 里报 401,检查.claude/settings.json里的ANTHROPIC_API_KEY是否和脚本用的是同一个 Key。

5.2 local proxy failed

报错长这样:

APIConnectionError: Connection error: local proxy failed to connect

这个报错通常和本地网络环境有关。先确认你没有在 shell 里设置HTTP_PROXY/HTTPS_PROXY这类变量,用env | grep -i proxy检查,有的话unset掉。然后确认能直接访问 API 端点:

curl -sS -o /dev/null -w "%{http_code}\n" https://taotoken.net/api

返回 200 或 401 都说明网络通,返回 000 说明连接没建立。如果公司网络有出站限制,找网络管理员确认放行。

5.3 reading choices 报错

报错长这样:

KeyError: 'choices'

或者:

IndexError: list index out of range

这通常发生在模型返回结构和你预期不一致的时候。排查:先打印完整响应print(resp)看结构;确认MODEL_ID是有效的模型标识,拼错了可能返回错误结构;确认请求里messages格式正确,role 和 content 都不能少。如果用的是流式响应,记得stream=True时要用for chunk in resp迭代,不能直接取resp.choices。

5.4 OAuth 相关报错

报错长这样:

OAuth token expired or invalid

Claude Code 在某些配置下会走 OAuth 流程。如果你用的是 API Key 模式,确认 settings 里没有残留的 OAuth 配置项。检查~/.claude/目录下是否有旧的凭证文件,必要时清理掉再重启 Claude Code。如果同时装了多个版本的 Claude Code,确认你改的是当前生效的那个配置文件。

5.5 三件套检查清单

只要涉及 Claude Code、Cline MCP、Codex 的 auth.json 这类集成,出问题先对照三件套:

项目正确值常见错误
Base URLhttps://taotoken.net/api多写/v1、带 UTM 参数
API Key控制台生成的sk-开头字符串复制带空格、用了旧 Key
Model ID如claude-sonnet-4-20250514拼写错误、用了不存在的模型名

Codex 的auth.json里如果配了base_url和api_key,同样按这三项核对。Cline 的 MCP 配置里,baseUrl和apiKey字段名可能不同,但值的要求一致。

5.6 Layer B 重写后文本质量下降

这不是报错,但是最常见的"坑"。重写轮数越多,原文风格被冲淡越明显。我的做法是:把max_rounds设成 2,重写后用 diff 工具对比原文和结果,人工确认关键术语没被改掉。如果内容里有代码块、专有名词、API 名称,重写前先用占位符保护起来,重写后再替换回去。这个保护逻辑可以加在layer_b_rewrite里,用正则把反引号包裹的内容抽出来暂存。

6. 把清除流程接进你的本地工作流

到这里,环境配好了,脚本能跑了,验证也过了。最后说说怎么把它变成日常习惯,而不是每次手动跑一遍。

最省事的方式是 Claude Code 的 PostToolUse hook。前面.claude/settings.json里已经配好了,Agent 每次 Write 或 Edit 文件后自动触发清理。你可以在 hook 脚本里加日志,记录每次清理的文件路径和剥离字符数,方便审计。Cursor 那边用.cursorrules约束,让 Agent 在写入后主动调用清理命令。

对于批量场景,比如你有一整个目录的 Markdown 草稿要处理,写个循环:

find ./drafts -name "*.md" -print0 | while IFS= read -r -d '' f; do python clean_pipeline.py "$f" --layer-b done

跑完再用verify.py批量扫一遍,确认没有残留。

关于 Layer B 的使用策略,我的建议是分内容类型:技术文档、API 说明、代码注释这类对措辞精度要求高的,只用 Layer A 加元数据剥离;营销文案、博客草稿、创意内容这类可以开 Layer B,重写带来的风格变化反而可能是加分项。图像和 PDF 一律开元数据剥离,像素域水印清除按需启用可选后端。

如果你要把这套流程用在长期编码或 Agent 任务里,Coding Plan 的通道 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 可以了解一下,统一 Key 在多个工具间轮换时省事。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,遇到配置问题先翻文档再排查。

最后留一个实用技巧:把清理脚本和验证脚本打包成一个 Makefile 目标,make clean FILE=draft.md一条命令跑完清除加验证,输出 JSON 报告。这样你既不用记参数,也能把每次清理的结果存档,方便回溯。工具的价值不在于"一键搞定",而在于流程透明、结果可审计——这也是 watermarks-remover 这类项目最值得借鉴的设计思路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询