通用 AI 溯源与水印净化中间件 研究产物——仅用于水印鲁棒性评估。
NullOrigin 是一个研究产物。其发布旨在支持关于水印鲁棒性的学术与独立研究,不用于任何其他目的。
水印方案是一种安全声明,而安全声明只有在有人尝试破解它时才有意义。本项目实现的相关文献——Kirchenbauer 等人关于 KGW 的研究、Krishna 等人关于释义攻击的研究、Boucher 和 Anderson 关于 Trojan Source 的研究——之所以存在,是因为研究者发布了可用的攻击方法,使防御者能够测量真实的鲁棒性,而不是仅仅假设其存在。本仓库正属于这一传统。
预期用途
非预期用途,且不提供支持
这里没有任何对代码运行方式的技术限制。这只是对其提供条款以及作者支持与不支持范围的声明。本软件按 "AS IS",不提供任何形式的担保——参见 LICENSE。
在根据本工具输出的任何数字得出结论之前,请先阅读范围与诚实局限。 其针对的多个方案无法通过公共检测器进行验证,README 将如实说明,而不会做任何暗示。
在根据本工具输出的任何数字得出结论之前,请先阅读本节。
| 层次 | 实际作用 |
|---|---|
| 不可见字符 | 完全有效。 零宽字符、双向控制符、变体选择符以及 Unicode Tags 区块的载荷都会被彻底移除,并报告计数。跨文字体系的同形字混淆(如西里尔/希腊字母显示为 ASCII)会被折叠。 |
| 文档元数据(.docx) | 完全有效。 作者、最后编辑者、修订次数、时间戳、模板和应用版本都会从 docProps 中清除,且格式逐字节保留。 |
| C2PA / EXIF / XMP | 完全有效。 图像会从原始像素样本重建到全新的容器中,因此已签名的 JUMBF 清单和所有元数据都会消失。已通过与带标记测试样本的测试验证。 |
| KGW 统计水印 | 完全取决于重写后端。 未配置本地模型时,统计水印仍然存在——工具会如实说明,而不会做任何暗示。 |
| SynthID-Text / SynthID-Image / Tree-Ring | 此处无法验证。 这些方案使用私钥和专有解码器。NullOrigin 会应用文献中描述的扰动,但不声称它们能够击败真实检测器,因为没有可用来测量的公开检测器。 |
| AudioSeal / SynthID-Audio | 此处无法验证,原因同上。 |
KGWStatisticalDetector 是 Kirchenbauer 等人基于空白 token 实现绿/红名单方案的数学上忠实、自洽的实现。它不是任何厂商生产环境水印的解码器——那些水印依赖私密密钥和模型自身的 BPE 词汇表。
其目的是让基准测试真实:KGWWatermarkEmbedder 植入一个真正的水印,流水线对其发起攻击,匹配的检测器测量实际的降幅。这是对该方案所受攻击的真实测量。它不适用于厂商的水印。
在每一步 $t$,词汇表 $V$ 通过以前文上下文为种子的哈希进行划分:
$$s_t = \text{Hash}(w_{t-k}, \dots, w_{t-1})$$
划分为大小为 $\gamma|V|$ 的绿名单 $G_t$ 和红名单 $R_t$。一个偏差 $\delta > 0$ 被加到绿色 logits 上:
$$\tilde{l}{t,v} = \begin{cases} l{t,v} + \delta, & v \in G_t \\ l_{t,v}, & v \in R_t \end{cases}$$
检测时统计绿色命中数。在 $H_0$ 下它们服从 $\text{Binomial}(T, \gamma)$,因此:
$$z = \frac{|S_G| - \gamma T}{\sqrt{T\gamma(1-\gamma)}}$$
当 $z > 4.0$($p < 3\times10^{-5}$)时被标记为合成内容。
为什么释义攻击有效: 水印完全存在于局部 n-gram 转移中。使用未加水印的模型重写表层形式会让每个位置重新播种。这是水印文献中的标准鲁棒性攻击。
为什么长度很重要: $z$ 随 $\sqrt{T}$ 增长。一段 100 token 的文本在 0.70 绿色比例下只能达到 $z \approx 3.9$——低于阈值。检测需要几百个 token,有意义的基准测试样本也是如此。
APP11 段、PNG 的 tEXt/iTXt 数据块或 WebP/AVIF 的 c2pa 盒中。由于签名覆盖了像素数据,从裸样本缓冲区重新编码即可移除签名,完全无需解析 JUMBF。亚阈值相位调制和低幅度频谱叠加。通过语音基频以上的相位随机化、非临界频段中的带阻陷波移位以及心理声学重新量化来进行攻击。
| Python | 3.10、3.11 或 3.12 |
| 操作系统 | Linux、macOS(Intel 和 Apple Silicon)、通过 WSL2 使用 Windows |
| 可选 | Ollama 或任何兼容 OpenAI 的服务器——文本去水印所必需 |
| 可选 | Docker 20.10+ 及 Compose v2 |
git clone https://github.com/rakib-nyc/nullorigin.git cd nullorigin
python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .
### 可选附加功能```bash
pip install -e ".[dev]" # pytest, pytest-asyncio, ruff — needed to run the tests
pip install -e ".[nli]" # torch + sentence-transformers, for the fidelity gate
pip install -e ".[metrics]" # torch, transformers, sentence-transformers
pip install -e ".[llama]" # llama-cpp-python for in-process GGUF inference
pip install -e ".[dev,metrics]"
没有
[nli],保真度门仅基于不变量运行——这仍然是一个真实的检查,但 对角色交换视而不见。参见 语义保真度。
nullorigin --version nullorigin --help pytest -q # requires the [dev] extra
---
## 🚀 快速开始
### 1. 设置本地重写模型
文本去水印需要一个未加水印的本地模型。如果没有这样的模型,NullOrigin 会移除不可见字符,但**会保留统计水印不变**——并且会明确说明这一点。```bash
ollama serve # in a separate terminal
ollama pull llama3.2:3b # or any instruct model you prefer
使用不同的模型?将 NullOrigin 指向它:```bash export NULLORIGIN_PARAPHRASER_MODEL=qwen3:4b export NULLORIGIN_PARAPHRASER_TIMEOUT=900 # reasoning models are slow
### 2. 启动代理```bash
nullorigin run
The input chunk is empty — no content was provided to translate. Please supply the chunk text and I’ll translate it into zh.```console NullOrigin 1.0.0 — proxy listening on 127.0.0.1:8080 providers: anthropic, gemini, openai text engine: unicode=True backend=ollama media: metadata=True stego=True telemetry: open (loopback) health: http://127.0.0.1:8080/health
### 3. 将你的客户端指向它```python
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="your-upstream-api-key")
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "Write an essay about privacy."}],
extra_headers={"x-nullorigin-provider": "openai"},
)
print(response.choices[0].message.content)
Anthropic:```python from anthropic import Anthropic
client = Anthropic(base_url="http://localhost:8080", api_key="your-upstream-api-key") message = client.messages.create( model="claude-sonnet-4-5", max_tokens=1024, messages=[{"role": "user", "content": "Write an essay about privacy."}], extra_headers={"x-nullorigin-provider": "anthropic"}, )
curl:```bash
curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "x-nullorigin-provider: openai" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"Hello"}]}'
流式(SSE)和 Gemini(/v1beta/models/...)以相同方式处理。x-nullorigin-provider 头选择上游,并在转发前被移除;你的认证头会原封不动地透传。
git clone https://github.com/rakib-nyc/nullorigin.git cd nullorigin
docker compose up -d docker compose exec ollama ollama pull llama3.2:3b # first run only curl http://localhost:8080/health
Compose 栈在私有桥接网络上运行 NullOrigin 以及一个 Ollama 边车容器。
代理容器绑定 `0.0.0.0` — 在容器内是正确的 — 并且只有端口 8080 会
发布到你的宿主机。
独立镜像:```bash
docker build -t nullorigin:1.0.0 .
docker run -d -p 8080:8080 \
-e NULLORIGIN_PARAPHRASER_BACKEND=none \
nullorigin:1.0.0
实用命令:```bash docker compose logs -f nullorigin docker compose down # stop docker compose down -v # stop and delete the Ollama model volume
## 🔒 在 localhost 之外部署
**NullOrigin 默认使用 `127.0.0.1`,并且在没有遥测令牌的情况下会拒绝绑定到公共接口。** 它会中继你的上游 API 凭据,因此这是有意为之:```console
$ nullorigin run --host 0.0.0.0
Error: Refusing to bind 0.0.0.0 without a telemetry token.