
研究専用AIウォーターマーク堅牢性ツールキット:ローカルリバースプロキシがC2PA/EXIF/XMP、Unicode、画像/音声ステゴ、OOXML/PDFメタデータを除去し、Trojan Sourceをスキャンする。
ユニバーサルAI由来・透かしサニタイゼーションミドルウェア 研究用アーティファクト — 透かしロバスト性評価専用。
NullOrigin は研究用アーティファクトです。透かしロバスト性に関する学術的・独立的な研究を支援するために公開されており、それ以外の目的はありません。
透かし方式はセキュリティ上の主張であり、セキュリティ上の主張は、誰かがそれを破ろうと試みて初めて意味を持ちます。このプロジェクトが実装する文献 — KGW に関する Kirchenbauer ら、パラフレーズ攻撃に関する Krishna ら、Trojan Source に関する Boucher & Anderson — は、研究者が実際に動作する攻撃を公開し、防御側が想定ではなく現実のロバスト性を測定できるようにしたからこそ存在します。このリポジトリが属するのは、そうした伝統です。
意図された用途
意図されておらず、サポートもされない用途
ここに記載されているのは、コードの実行方法に関する技術的制御ではありません。本ソフトウェアが提供される条件と、その作者がサポートする/しないことの表明です。本ソフトウェアは、いかなる種類の保証もなく**「AS IS」(現状のまま)**で提供されます — LICENSE を参照してください。
このツールが出力する数値から何らかの結論を導く前に、範囲と誠実な限界をお読みください。 対象とする方式のいくつかは公開されている検出器では検証できません。README は、それを暗に示すのではなく明言しています。
このツールが出力する数値から結論を導く前に、これを読んでください。
KGWStatisticalDetector は、Kirchenbauer らによる空白トークン上の green/red リスト方式を数学的に忠実に、かつ自己整合的に実装したものです。これは、いかなるベンダーの本番透かしのデコーダでもありません。そうしたデコーダは秘密鍵とモデル独自の BPE 語彙に依存します。
その目的は、ベンチマークを現実のものにすることです。KGWWatermarkEmbedder が本物の透かしを埋め込み、パイプラインがそれを攻撃し、対応する検出器が実際の低減量を測定します。これはこの方式に対する攻撃の真の測定です。ベンダーの透かしには適用できません。
語彙 $V$ は、各ステップ $t$ において、直前の文脈をシードとしたハッシュによって分割されます:
$$s_t = \text{Hash}(w_{t-k}, \dots, w_{t-1})$$
サイズ $\gamma|V|$ の green リスト $G_t$ と red リスト $R_t$ に分割されます。green ロジットにはバイアス $\delta > 0$ が加算されます:
$$\tilde{l}{t,v} = \begin{cases} l{t,v} + \delta, & v \in G_t \\ l_{t,v}, & v \in R_t \end{cases}$$
検出は green ヒットを数えます。$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}$ に比例して増大します。green 割合 0.70 の 100 トークンの文章では $z \approx 3.9$ にしか達しません — 閾値未満です。検出には数百トークンが必要であり、意味のあるベンチマークフィクスチャにも同様に数百トークンが必要です。
APP11 セグメント、PNG の tEXt/iTXt チャンク、または WebP/AVIF の c2pa ボックス内にある署名付きマニフェスト。署名はピクセルデータを対象としているため、素のサンプルバッファから再エンコードすれば、JUMBF を一切解析せずに除去できます。閾値以下の位相変調と低振幅のスペクトル付加です。音声基本周波数より上の位相ランダム化、非臨界帯域におけるバンドストップ・ノッチのシフト、心理音響的再量子化によって攻撃します。
| Python | 3.10、3.11、または 3.12 |
| OS | Linux、macOS(Intel および Apple Silicon)、WSL2 経由の Windows |
| 任意 | Ollama または OpenAI 互換サーバー — テキストの透かし除去に必要 |
| 任意 | Compose v2 を備えた Docker 20.10+ |
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
Input content is missing — the chunk to translate was not included after "INPUT:". Please provide the source text for chunk 13 of 59.```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.
Choose one:
- bind loopback: nullorigin run --host 127.0.0.1
- set a token: export NULLORIGIN_TELEMETRY_TOKEN=$(openssl rand -hex 32)
- accept the risk: nullorigin run --host 0.0.0.0 --allow-public-bind
適切に公開するには:```bash export NULLORIGIN_TELEMETRY_TOKEN=$(openssl rand -hex 32) nullorigin run --host 0.0.0.0 --port 8080
then put it behind nginx, Caddy, or Traefik providing **TLS termination**, **rate
limiting**, and an **auth layer**.
### Threat model
NullOrigin is a **local reverse proxy that relays your upstream API credentials**. That
single fact drives its security posture.
| Control | Default | Why |
| --- | --- | --- |
| Bind address | `127.0.0.1` | Loopback only; a public bind is refused unless a telemetry token is set or `--allow-public-bind` is passed. |
| `/telemetry`, `/telemetry/reset` | Open on loopback | Gated by `X-NullOrigin-Token`, constant-time compared, whenever `proxy.telemetry_token` is set. |
| `/health` | Always open | Container probes need it; exposes version and enabled engines, no secrets. |
| Request body size | 100 MiB | The proxy buffers bodies to forward them; larger input is rejected with `413`. |
| SSE buffer | 1 MiB | An upstream that never terminates a frame is flushed, not buffered indefinitely. |
| Container user | non-root | The proxy needs no elevated privileges. |
Known limitations, by design and not defects:
* **No TLS.** It forwards `Authorization` and `x-api-key` verbatim over plain HTTP. Put
it behind a reverse proxy that terminates HTTPS on any untrusted network.
* **No authentication on the proxy path.** Anyone who can reach the port can proxy
through it, using their own credentials — NullOrigin neither stores nor injects keys.
* **No rate limiting.** Apply it at the reverse proxy.
* **Credentials are never persisted.** No API key is written to disk or logs; telemetry
counts requests and sanitization events only.
* Upstream TLS verification stays on, and redirects are not followed.
To report a security issue, email **[email protected]** with `[NullOrigin Security]` in
the subject.
Telemetry with a token set:```bash
curl -H "X-NullOrigin-Token: $NULLORIGIN_TELEMETRY_TOKEN" http://localhost:8080/telemetry
/health は決してゲートされないため、コンテナプローブは動作し続けます。
nullorigin run [--host H] [--port P] [--config FILE] [--allow-public-bind] nullorigin purge INPUT -o OUTPUT [--verify] [--no-paraphrase] [--flatten-typography] nullorigin inspect INPUT [--json] nullorigin benchmark [--section text|media|audio] [-o report.json] nullorigin build-datasets [--root DIR] nullorigin test [pytest args...]
### 対応フォーマット
| 種類 | 拡張子 | 備考 |
| --- | --- | --- |
| **画像** | `.png` `.jpg` `.jpeg` `.jfif` `.webp` `.tif` `.tiff` `.bmp` `.gif` `.ico` `.avif` `.jp2` | すべてのPILモード(RGB、RGBA、L、LA、P、1、I;16、CMYK、YCbCr)に対応。アニメーションGIF/WebPとマルチページTIFFは、すべてのフレームとそのタイミングを保持します。64px未満の画像は正確な寸法を維持します。 |
| **音声** | `.wav` `.wave` | 8/16/32ビット整数、32ビット浮動小数点。モノラルからマルチチャンネルまで、任意のサンプルレートに対応。ゼロ長ファイルもラウンドトリップします。 |
| **文書** | `.docx` `.docm` `.dotx` `.pptx` `.pptm` `.xlsx` `.xlsm` | OOXMLの3つの方言すべてに対応。テキストランは本文、ヘッダー、フッター、脚注、注記、共有文字列にわたってサニタイズされ、`docProps`メタデータは削除され、その他のすべてのパーツはバイト単位でコピーされます。 |
| **PDF** | `.pdf` | `/Info`辞書、XMPパケット、埋め込み添付ファイル、JavaScriptを削除。ページ、テキスト、ジオメトリは保持されます。パスワード保護されたファイルは拒否されます。後述の注意事項を参照してください。 |
| **ソースコード** | `.py` `.js` `.ts` `.go` `.rs` `.java` `.c` `.cpp` `.rb` `.php` `.sh` `.sql` + さらに50以上 | Trojan Source(トロイのソース)とホモグリフのスキャン。**NFKC変換なし、言い換えなし** — 後述参照。 |
| **テキスト** | その他デコード可能なもの | UTF-8、UTF-8 BOM、UTF-16、UTF-32、CP1252、Latin-1 — 自動検出され、**同じエンコーディングで書き戻されます**。 |
その他はすべて、UTF-8として読み取られて破損するのではなく、**具体的なガイダンス付きで拒否**されます — `.mp3`は`ffmpeg -i in.mp3 out.wav`を案内され、レガシーな`.doc`/`.ppt`/`.xls`はOOXMLとして再保存するよう案内されます。拒否されたファイルが出力を生成することは決してありません。
上記のすべてのフォーマットにわたる69ファイルのコーパスで検証済み: **59件が正常に処理され、10件がクリーンに拒否され、クラッシュゼロ、破損出力ゼロ。**
### コードはリライターから除外される
アシスタントの応答は散文とコードが1つの文字列に混在しています。全体をパラフレーズモデルに渡すと、散文と一緒にコードも書き換えられます — そしてどちらにせよzスコアは低下するため、後段の処理は何も気づきません。
したがって、応答は何かを書き換える前にセグメント分割されます:
| セグメント | 処理 |
| --- | --- |
| 散文 | Unicodeクリーニング後、書き換え |
| フェンスで囲まれたブロック(``` と ~~~) | 不可視文字と双方向(bidi)文字を除去。**NFKC変換なし、決して書き換えなし。** |
| インラインの `` `code` `` スパン | 同様 |
これはストリーミング経路でも同様で、フェンスがあるデルタで開き、数デルタ後に閉じます。境界にまたがるデルタは行単位で分割されるため、閉じる ``` とその後の散文は異なる扱いを受けます。閉じられていないフェンスはフェイルセーフに動作します: 残りの部分は書き換えられるのではなく保護されます。
以前の動作が必要な場合は、`text.protect_code_blocks: false`で無効にできます。
### ソースコードファイル: 透かし除去ではなくセキュリティスキャン
**AI生成のソースコードには透かしはありません。** コード出力に透かしを入れるプロバイダーは存在せず、公開された検出器も存在しません。透かしを除去できると主張する者は、何かを売りつけているだけです。
ソースコードに*実際に*存在するのは、現実的で公表された攻撃対象領域です:
* **Trojan Source**([CVE-2021-42574](https://nvd.nist.gov/vuln/detail/CVE-2021-42574)、Boucher & Anderson 2021)— 双方向制御文字は、コードの*コンパイル*方法を変えることなく*表示*方法を並べ替えます。レビュアーがあるプログラムを承認しても、コンパイラは別のプログラムをビルドします。
* **ホモグリフ識別子**([CVE-2021-42694](https://nvd.nist.gov/vuln/detail/CVE-2021-42694))— ラテン文字の`a`の代わりにキリル文字の`а`を使うと、見た目が同一の2つの名前が生成されます。```console
$ nullorigin purge auth.py -o auth_clean.py --verify
Scanning source file auth.py...
bidi controls removed: 4
invisible chars removed: 0
TROJAN SOURCE DETECTED (CVE-2021-42574): 4 bidirectional control character(s).
This file rendered differently than it compiled. Review the diff.
Findings:
CRITICAL line 3:25 U+202E RIGHT-TO-LEFT OVERRIDE — reorders displayed text
if access_level != "user // Check if admin":
パージ後、その行は if access_level != "user // Check if admin": と読めます —
「コメント」はずっと文字列の中にあったのです。
コードパスが意図的に行わない3つのこと — 一般的なテキストパスはこれら3つをすべて行っており、それぞれがソース上のバグです。
"Hello" は
"Hello" に、"office" は "office" になりました。これはプログラムが比較・ハッシュ・
送信する内容を変えます。а を折り畳むと、コンパイラが現在別物として扱う2つの識別子
が統合され、動作を静かに変えます。深刻度は、実際の攻撃シグネチャである混在スクリプトトークン (totаl) に対してのみ MEDIUM です。
別の文字体系で完全に書かれた単語
は通常の外国語テキストであり、INFO と評価されます。レビュー後に
--fold-homoglyphs-in-code でオプトインしてください。inspect --json は、検出結果ごとに深刻度・行・列・コードポイントを出力するため、pre-commit や PR ゲート
として CI に組み込めます。
削除されるもの(検証済み): /Info ディクショナリ(Author, Title, Subject, Keywords,
Creator, Producer, CreationDate, ModDate)、/Root/Metadata 内の XMP パケット、埋め込み
ファイル添付、およびドキュメントレベルの JavaScript。ページ、テキスト、ページジオメトリは
正確に保持されます。この操作は冪等でバイト単位で安定しています。
検出されるが削除されないもの: ページコンテンツストリーム内の不可視文字。PDF
はフォント固有のエンコーディングでテキストをグリフごとに描画します — CIDキー付きフォントのゼロ幅スペースは
リテラルな U+200B ではなく2バイトのグリフインデックスです — そのため、汎用的な書き換えは
クリーンにするどころかレイアウトを壊します。inspect は件数を報告し、purge
は沈黙せずに警告を出力します。沈黙は「何もなかった」と読まれるためです。
削除するには、テキストを抽出し、nullorigin purge を実行し、PDF を
再生成してください。
--strip-annotations は利用可能ですが、デフォルトではオフです。アノテーションにはリンクと
フォームフィールドが含まれ、コメントだけではありません。そのため、削除するとドキュメントの動作が変わります。
emダッシュ、カール引用符、省略記号は一般的なワードプロセッサの出力です。NullOrigin は
デフォルトでこれらを保持し、本当の検出結果とは別に報告します。なぜなら、
それらを平坦化すると、何もサニタイズせずにドキュメントを劣化させるだけだからです。
特に ASCII 出力が必要な場合は --flatten-typography を使用してください。
クロススクリプトの紛らわしい文字は別です — 英語テキストの単語中にあるキリル文字 о には
正当な用途がありません — そのため、これらはデフォルトで折り畳まれます。
--verify は成功を断言するのではなく、前後の測定値を報告します:```console
$ nullorigin purge article.txt -o clean.txt --verify
Cleaning text structure and token transitions in article.txt...
removed 14 invisible characters, folded 3 homoglyphs
applying semantic restructuring via ollama backend...
restructuring complete
Saved clean text to clean.txt
Verification (KGW statistical detector): z-score before: +5.3021 (p=5.73e-08) z-score after: +0.8874 (p=0.187) detected before/after (z>4.0): True -> False
バックエンドに到達できない場合、それはstderrに警告として報告されます — 警告なしの
フォールバックは、成功したサニタイゼーションとまったく同じに見えるでしょう。
---
## 📊 ベンチマーキング```bash
nullorigin build-datasets
nullorigin benchmark
すべての値はその場で測定されます: テキストはKGWWatermarkEmbedderで透かしが入れられ、
実際のパイプラインに通され、対応する検出器で再スコアリングされます。ランナーは
しきい値が満たされない場合は非ゼロで終了し、その理由を説明します。
しきい値(プロジェクト指示より):
| 指標 | 目標 |
|---|---|
| サニタイズ後zスコア | $\lvert z\rvert \le 1.5$ |
| 意味的類似度 | $\ge 0.92$ |
| 画像SSIM | $\ge 0.95$ |
| 画像PSNR | $\ge 36$ dB |
datasets/text/watermarked_kgw.json 上の全文セクションを、Ollama
(qwen3:4b)経由でMシリーズのMacBook上で書き換え(1パッセージあたり約150秒):```text
sample z_before z_after reduced detected
kgw_000 4.212 -0.065 4.277 no
kgw_001 5.297 0.484 4.813 no
kgw_002 6.120 -0.482 6.601 no
kgw_003 4.711 1.271 3.440 no
kgw_004 5.696 0.209 5.486 no
invisible_payload -0.447 1.091 -1.538 no
mean z: 4.2647 -> 0.4182 max |z| after: 1.271 (target: <= 1.5) still detected at z > 4.0: 0 of 5 invisible chars remaining: 0
pass_z_threshold: PASS pass_no_detection: PASS pass_unicode_purge: PASS OVERALL: PASS (3/3)
すべての透かし入りサンプルは、検出された状態から未検出の状態へ移行しました。`kgw_003` は z = 1.271 にあり、閾値を下回っていますが、それに最も近い値です。これがこの攻撃の正直な姿です。すなわち、統計的なものであり、保証ではありません。
メディア(画像フィクスチャ全体で測定):```text
sample ssim psnr_dB meta_clear
c2pa_tagged.png 0.9950 46.84 yes
exif_tagged.jpg 0.9690 40.54 yes
clean_control.png 0.9951 46.90 yes
両方の画像しきい値を通過します(SSIM ≥ 0.95、PSNR ≥ 36 dB)。数値はモデル、ハードウェア、および文章によって異なります。
事実を変えるリライトは、ウォーターマーク指標では忠実なリライトとまったく同じスコアになります。これに対する明白なチェックは機能せず、あまり明白でないチェックも同様に機能しません。6つのドリフトケースと忠実な対照を測定しました:
語彙の重複は逆転している。 意味を破壊するすべての編集は、忠実なリライトよりも高いスコアを獲得しました。なぜなら、優れたパラフレーズはソースと共有するn-gramが少ないのに対し、破壊されたものはほぼすべてを共有するからです。
埋め込みコサインはこれを解決しない。 6つの破損ケースのうち3つが0.92のしきい値を通過します。"Alice paid Bob" と "Bob paid Alice" は同じ単語の袋(bag of words)であり、0.985を獲得します。"must not disable" → "must disable" は0.947を獲得します。文埋め込みは話題の関連性をエンコードするのであって、真実性をエンコードするのではありません。
したがって、忠実性は2つの層でチェックされ、そのどちらもコサインではありません:
negation count changed: 1 → 0)。法助動詞と数量詞は意味クラスで比較されるため、may → might は合格し、may → must は不合格になります。すべてのエンティティが残る役割の入れ替えには気づきません。nullorigin[nli] が必要です。これがない場合、制限は隠されずに報告されます。損傷したテキストがすでに返されてしまった場合、事後的にドリフトを測定しても役に立ちません。チェックが失敗すると、より低い温度で再試行されます — ドリフトは温度に起因するためです — そして再試行の予算を使い切ると、元のテキストを ok=False と理由とともに返します。```yaml
text:
fidelity:
enabled: true
max_retries: 2
temperature_step: 0.25
use_nli: true
nli_threshold: 0.5
これはまた、攻撃とリスクが一つのダイヤルを共有することを意味する:温度を上げると
zスコアが下がり、*かつ* ドリフト率が上がる。ベンチマークはこれらを独立したチェックとしてではなく、
一緒に報告する。
### その他のメトリクス
* **Perplexity** — `torch` + `transformers` を使用した実際の GPT-2 PPL、それ以外の場合は
`unigram_entropy_proxy` を使用し、近似としてフラグ付けされ、公開されたPPLとは**比較できません**。
* **コサイン類似度** は参照専用として引き続き `mean_cosine_or_lexical` として報告されます。
上記の表の理由により、これはもはや合格/不合格のゲートではありません。
## ⚙️ 構成
解決順序(低いものから高いものへ):
1. 組み込みのデフォルト
2. `nullorigin.yaml`(`./`、`../`、`/app/`、または `$NULLORIGIN_CONFIG` で検索)
3. `NULLORIGIN_*` 環境変数
4. 明示的なCLIフラグ
### 主要な設定
| 設定 | デフォルト | 備考 |
| --- | --- | --- |
| `proxy.host` | `127.0.0.1` | ループバック。テレメトリトークンなしでは公開バインドは拒否されます。 |
| `proxy.port` | `8080` | |
| `proxy.default_provider` | `openai` | `x-nullorigin-provider` ヘッダーが送信されない場合に使用されます。 |
| `proxy.telemetry_token` | `""` | `/telemetry` と `/telemetry/reset` を保護します。 |
| `proxy.max_request_bytes` | `104857600` | 100 MiB; それを超えるボディは `413` を受け取ります。 |
| `text.paraphraser.backend` | `ollama` | `none` \| `ollama` \| `openai_compatible` \| `llama_cpp` \| `lexical`。`none` は統計的透かしをそのまま残します。`lexical` はモデルを必要としませんが、はるかに弱い攻撃です。 |
| `text.clean_unicode` | `true` | ゼロ幅およびTagsブロックの除去。 |
| `text.fold_homoglyphs` | `true` | キリル文字/ギリシャ文字の紛らわしい文字をASCIIに変換。 |
| `text.stream_window_tokens` | `40` | ストリーミングスパンが書き換えられる前にバッファリングされる差分。 |
| `media.crop_mode` | `trim` | `trim` はリサンプリングなしで座標をシフトします。`resample` は正確な寸法を復元しますが、0.5%のクロップでもおよそSSIM 0.81 / PSNR 31 dBのコストがかかります。`none` は幾何学的パスを無効にします。 |
| `audio.low_cut_hz` | `800.0` | この周波数以下の位相は了解度のために保持されます。 |
### 環境変数```bash
NULLORIGIN_CONFIG # path to nullorigin.yaml
NULLORIGIN_HOST # bind address
NULLORIGIN_PORT
NULLORIGIN_TELEMETRY_TOKEN
NULLORIGIN_MAX_REQUEST_BYTES
NULLORIGIN_DEFAULT_PROVIDER
NULLORIGIN_PARAPHRASER_BACKEND # none | ollama | openai_compatible | llama_cpp | lexical
NULLORIGIN_PARAPHRASER_ENDPOINT # alias: NULLORIGIN_OLLAMA_ENDPOINT
NULLORIGIN_PARAPHRASER_MODEL
NULLORIGIN_PARAPHRASER_MODEL_PATH # llama_cpp GGUF path
NULLORIGIN_PARAPHRASER_API_KEY
NULLORIGIN_PARAPHRASER_TIMEOUT
NULLORIGIN_PARAPHRASER_TEMPERATURE
NULLORIGIN_CLEAN_UNICODE
NULLORIGIN_FOLD_HOMOGLYPHS
NULLORIGIN_PURGE_METADATA
NULLORIGIN_DISRUPT_STEGO
NULLORIGIN_DISRUPT_AUDIO
model 'llama3.2:3b' not found
設定されたモデルがプルされていません。ollama list を実行して何があるか確認し、ollama pull llama3.2:3b を実行するか、NULLORIGIN_PARAPHRASER_MODEL を既に保持しているモデルに設定してください。
WARNING: ollama backend unavailable (ReadTimeout)
リライトが text.paraphraser.timeout_seconds(デフォルト 120 秒)を超えました。qwen3 などの推論モデルは、CPU 上では段落あたり 150 秒以上かかることがよくあります。次のようにして値を引き上げてください: export NULLORIGIN_PARAPHRASER_TIMEOUT=900。または、より小さな instruct モデルを使用してください。
nullorigin benchmark が pass_no_detection: FAIL で exit 1 になる
意図した動作です。リライトバックエンドに到達できなかったため、unicode レイヤーのみが実行され、統計的ウォーターマークが残りました。Ollama を起動するか、依存関係なしで比較するためにバックエンドを lexical に設定してください。
semantic_check: INCONCLUSIVE
[metrics] エクストラがない場合は想定どおりです。Metric honesty を参照してください。
Error: Refusing to bind 0.0.0.0 without a telemetry token
意図的です。Deploying beyond localhost を参照してください。
Multiple top-level packages discovered in a flat-layout
古いチェックアウトを使用しています。pyproject.toml には明示的なパッケージリストが設定されています。最新版をプルしてください。
非同期テストで、async プラグインがないという UsageError が報告される
意図的です。これがないと、pytest は async def テストを await せずに合格として報告します。pip install -e ".[dev]" を実行してください。
Docker: compose up 直後に curl: (7) Failed to connect
ヘルスチェックには 10 秒の開始期間があります。待ってから、docker compose logs nullorigin を確認してください。
Client / Application
|
[http://localhost:8080/v1/...]
v
+===================================================+
| NULLORIGIN CORE PROXY |
| HTTP/SSE interceptor · provider schema adapter |
| /health · /telemetry · transparent auth passthru |
+===================================================+
|
[request forwarded unmodified]
v
Upstream Provider API (Anthropic / OpenAI / Gemini)
|
[watermarked payload]
v
+===================================================+
| SANITIZATION PIPELINE ROUTER |
+===================================================+
/ | \
(text/JSON+SSE) (image/*) (audio/wav) v v v +----------------+ +------------------+ +------------------+ | MODULE B: TEXT | | MODULE C: MEDIA | | MODULE D: AUDIO | | unicode purge | | C2PA/EXIF scrub | | phase randomize | | homoglyph fold | | DWT threshold | | notch shifting | | KGW detector | | Fourier phase | | psychoacoustic | | SLM rewriter | | dither | | requantization | +----------------+ +------------------+ +------------------+ \ | / +----------------+---------------------+ v Schema reconstruction (SSE framing preserved) v Sanitized stream / file
### レイアウト```text
nullorigin/
├── cli.py # run, purge, inspect, benchmark, build-datasets, test
├── config.py # Pydantic v2 settings + env overrides
├── proxy/
│ ├── server.py # FastAPI reverse proxy, /health, /telemetry
│ ├── interceptors.py # SSE frame parser + sliding-window rewriter
│ ├── telemetry.py # thread-safe runtime counters
│ └── schemas.py # provider request/response models
├── engines/
│ ├── text/
│ │ ├── unicode_cleaner.py # invisible chars, Tags block, homoglyphs
│ │ ├── paraphraser.py # pluggable rewrite backends
│ │ └── kgw_detector.py # detector + Viterbi embedder
│ ├── media/
│ │ ├── c2pa_remover.py # JUMBF/EXIF/XMP stripping + inspection
│ │ └── stego_breaker.py # DWT thresholding, Fourier phase, dither
│ └── audio/
│ └── audio_cleaner.py # phase randomization, notch shifting
└── evaluation/
├── metrics.py # SSIM, PSNR, PPL, semantic similarity
├── datasets.py # deterministic fixture generation
└── runner.py # measured benchmark harness
pytest -q
384 テスト。このスイートは、敵対的なチャンク境界に対する SSE フレームパーサー、プロキシストリーミングのライフサイクル、すべての PIL 画像モード、閉形式の値とブルートフォース参照実装の両方に対する SSIM、およびデータセットの透かし検出可能性をカバーしています。
非同期テストは、非同期プラグインがインストールされていない場合、黙ってスキップするのではなく、明示的に失敗します。
---
## 📖 引用と再利用
Apache-2.0 ライセンスのもとで提供されており、著作権表示と **Muhammad Rakibul Islam** への帰属が保持される限り、使用、変更、再配布が許可されています。[LICENSE](https://github.com/rakib-nyc/nullorigin/blob/HEAD/LICENSE) と [NOTICE](https://github.com/rakib-nyc/nullorigin/blob/HEAD/NOTICE) を参照してください。
この成果が出版物で使用される場合は、次のように引用してください:```bibtex
@software{islam_nullorigin_2026,
author = {Islam, Muhammad Rakibul},
title = {{NullOrigin}: Universal AI Provenance and Watermark
Sanitization Middleware},
year = {2026},
version = {1.2.0},
url = {https://github.com/rakib-nyc/nullorigin},
note = {Research artifact for watermarking robustness evaluation}
}
このリポジトリは完成した研究成果物として公開されており、プルリクエストは受け付けていません。 ライセンスの条件に基づき、自由にフォークできます。質問や発見は [email protected] まで電子メールで歓迎します。
バージョン1.0.0。このスイートは仕様に対して完全であり、十分にテスト済みですが、 以下の既知の制約があります:
[nli]がないと、すべての
エンティティを保持するロールの入れ替えは検出できず、レポートにもその旨が記載されています。CHANGELOG.md でリリース履歴を参照してください。
このリポジトリは、統計調査、プライバシー評価、ウォーターマーキングの堅牢性ベンチマーク、および 暗号耐性テストのために公開された研究グレードの成果物です。```text Copyright 2026 Muhammad Rakibul Islam [email protected]
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
**保証はありません。** 本ソフトウェアは「現状のまま」で提供され、明示または黙示を問わず、
いかなる種類の保証もありません。
| レイヤー | 実際の動作 |
|---|
| 不可視文字 | 完全に有効。 ゼロ幅文字、双方向制御文字、変異選択子、Unicode Tags ブロックのペイロードは完全に除去され、その数が報告されます。異なるスクリプト間のホモグリフ紛らわし文字(キリル文字/ギリシャ文字が ASCII として描画されるもの)は畳み込まれます。 |
| 文書メタデータ (.docx) | 完全に有効。 docProps から作成者、最終編集者、リビジョン数、タイムスタンプ、テンプレート、アプリケーションのバージョンが消去され、書式はバイト単位で保持されます。 |
| C2PA / EXIF / XMP | 完全に有効。 画像は生のピクセルサンプルから新しいコンテナとして再構築されるため、署名付き JUMBF マニフェストとすべてのメタデータが消去されます。タグ付きフィクスチャに対するテストで検証済み。 |
| KGW 統計透かし | 書き換えバックエンドに完全に依存します。 ローカルモデルが設定されていない場合、統計透かしは生き残ります — このツールは、それを暗に示すのではなく明言します。 |
| SynthID-Text / SynthID-Image / Tree-Ring | ここでは検証できません。 これらは秘密鍵とプロプライエタリなデコーダを使用します。NullOrigin は文献に記載された摂動を適用しますが、実際の検出器を無効化するという主張は一切しません。比較測定できる公開検出器が存在しないためです。 |
| AudioSeal / SynthID-Audio | ここでは検証できません。理由は同じです。 |
| ケース | lexical_f1 | 埋め込みコサイン | 双方向NLI |
|---|
| 否定の欠落 | 0.70 | 0.77 ✓ | 0.000 ✓ |
| 数値 5 → 50 | 0.82 | 0.81 ✓ | 0.000 ✓ |
| エンティティ/役割の入れ替え | 0.81 | 0.985 ✗ | 0.000 ✓ |
| 数量詞 all → some | 0.88 | 0.91 ✓ | 0.000 ✓ |
| ヘッジの削除 | 0.27 | 0.953 ✗ | 0.011 ✓ |
| "must not" → "must" | 0.83 | 0.947 ✗ | 0.000 ✓ |
| 忠実なリライト | 0.33 | 0.931 ✓ | 0.998 ✓ |