
연구 전용 AI 워터마크 견고성 툴킷: 로컬 리버스 프록시가 C2PA/EXIF/XMP, Unicode, 이미지/오디오 스테가노그래피, OOXML/PDF 메타데이터를 제거하고 Trojan Source를 스캔합니다.
범용 AI 출처 및 워터마크 정화 미들웨어 연구 산출물 — 워터마킹 견고성 평가 전용입니다.
NullOrigin은 연구 산출물입니다. 워터마킹 견고성에 대한 학술적·독립적 연구를 지원하기 위해 공개되었으며, 그 외의 목적은 없습니다.
워터마킹 기법은 보안 주장이며, 보안 주장은 누군가 그것을 깨보려고 시도할 때만 의미가 있습니다. 이 프로젝트가 구현하는 문헌들 — KGW에 대한 Kirchenbauer 등, 패러프레이즈 공격에 대한 Krishna 등, Trojan Source에 대한 Boucher & Anderson — 은 연구자들이 실제 견고성을 측정할 수 있도록 가정하는 대신 작동하는 공격을 게시했기 때문에 존재합니다. 이 저장소가 속한 전통이 바로 이것입니다.
의도된 용도
의도되지 않았으며 지원되지 않는 용도
여기의 어떤 것도 코드가 실행되는 방식에 대한 기술적 통제 장치가 아닙니다. 이는 이 소프트웨어가 제공되는 조건과 작성자가 지원할 것과 지원하지 않을 것에 대한 진술입니다. 이 소프트웨어는 어떠한 종류의 보증도 없이 "있는 그대로" 제공됩니다 — LICENSE 참조.
이 도구가 출력하는 숫자에서 어떤 결론을 내리기 전에 범위 및 정직한 한계 를 읽으십시오. 대상이 되는 여러 기법은 공개 탐지기로 검증할 수 없으며, README는 그 반대를 암시하는 대신 이를 명시합니다.
이 도구가 출력하는 숫자에서 결론을 내리기 전에 이 내용을 읽으십시오.
KGWStatisticalDetector는 공백 토큰에 대한 Kirchenbauer 등의 그린/레드 리스트 기법을 수학적으로 충실하게 구현한 자기 일관적 구현체입니다. 이는 어떤 공급업체의 프로덕션 워터마크용 디코더가 아닙니다 — 그러한 워터마크는 비공개 비밀과 모델 고유의 BPE 어휘를 키로 사용합니다.
그 목적은 벤치마크를 실제로 만드는 것입니다: KGWWatermarkEmbedder가 진짜 워터마크를 심고, 파이프라인이 이를 공격하며, 일치하는 탐지기가 실제 감소량을 측정합니다. 이는 이 기법에 대한 공격의 실제 측정값입니다. 공급업체의 워터마크에는 적용되지 않습니다.
어휘 $V$는 각 단계 $t$에서 이전 문맥에 시드된 해시에 의해 분할됩니다:
$$s_t = \text{Hash}(w_{t-k}, \dots, w_{t-1})$$
크기 $\gamma|V|$의 그린 리스트 $G_t$와 레드 리스트 $R_t$로 나뉩니다. 바이어스 $\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}$$
탐지는 그린 적중 수를 셉니다. $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-그램 전이에 존재합니다. 워터마크가 없는 모델로 표면 형태를 다시 쓰면 모든 위치가 다시 시드됩니다. 이는 워터마킹 문헌에서 표준적인 견고성 공격입니다.
왜 길이가 중요한가: $z$는 $\sqrt{T}$에 비례하여 증가합니다. 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 호환 서버 — 텍스트 디워터마킹에 필요 |
| 선택 사항 |
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]가 없으면 충실도 게이트는 불변식(invariants)만으로 실행됩니다. 여전히 실제 검사이지만 역할 교체(role swaps)에는 눈이 멀게 됩니다. 의미론적 충실도를 참조하세요.
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
(Empty response due to missing input)```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"}]}'
Streaming (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
---
## 🔒 로컬호스트 밖으로 배포하기
**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
그런 다음 **TLS 종료**, **속도
제한** 및 **인증 계층**을 제공하는 nginx, Caddy 또는 Traefik 뒤에 배치하세요.
### 위협 모델
NullOrigin은 **업스트림 API 자격 증명을 중계하는 로컬 리버스 프록시**입니다. 이
단순한 사실이 보안 태세를 결정합니다.
| 제어 | 기본값 | 이유 |
| --- | --- | --- |
| 바인드 주소 | `127.0.0.1` | 루프백 전용. 텔레메트리 토큰이 설정되거나 `--allow-public-bind`가 전달되지 않으면 공개 바인드가 거부됩니다. |
| `/telemetry`, `/telemetry/reset` | 루프백에서 열림 | `proxy.telemetry_token`이 설정되면 `X-NullOrigin-Token`으로 게이트되고 상수 시간 비교가 수행됩니다. |
| `/health` | 항상 열림 | 컨테이너 프로브가 필요로 합니다. 버전과 활성화된 엔진을 노출하며 비밀은 포함하지 않습니다. |
| 요청 본문 크기 | 100 MiB | 프록시는 본문을 버퍼링하여 전달하며, 더 큰 입력은 `413`으로 거부됩니다. |
| SSE 버퍼 | 1 MiB | 프레임을 종료하지 않는 업스트림은 무기한 버퍼링되지 않고 플러시됩니다. |
| 컨테이너 사용자 | non-root | 프록시는 상승된 권한이 필요 없습니다. |
알려진 제한 사항(설계상 의도된 것이지 결함이 아님):
* **TLS 없음.** `Authorization` 및 `x-api-key`를 일반 HTTP로 그대로 전달합니다. 신뢰할 수 없는 네트워크에서는 HTTPS를 종료하는 리버스 프록시 뒤에
배치하세요.
* **프록시 경로에 대한 인증 없음.** 포트에 도달할 수 있는 사람은 누구나
자신의 자격 증명을 사용하여 프록시할 수 있습니다. NullOrigin은 키를 저장하거나 주입하지 않습니다.
* **속도 제한 없음.** 리버스 프록시에서 적용하세요.
* **자격 증명은 절대 영구 저장되지 않습니다.** 디스크나 로그에 API 키가 기록되지 않으며, 텔레메트리는 요청 및 정화 이벤트 수만
집계합니다.
* 업스트림 TLS 검증은 계속 활성화되며, 리다이렉트는 따르지 않습니다.
보안 문제를 신고하려면 **[email protected]**으로 이메일을 보내고 제목에 `[NullOrigin Security]`를
포함하세요.
토큰이 설정된 텔레메트리:```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비트 부동소수점; 모노부터 멀티채널까지; 모든 샘플 레이트. 길이가 0인 파일도 그대로 처리됩니다. |
| **문서** | `.docx` `.docm` `.dotx` `.pptx` `.pptm` `.xlsx` `.xlsm` | 세 가지 OOXML 방언 모두 지원. 본문, 머리글, 바닥글, 각주, 주석 및 공유 문자열 전반의 텍스트 런이 정화되고; `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개는 깔끔하게 거부, 크래시 0건, 손상된 출력 0건.**
### 코드는 재작성기에서 제외됩니다
어시스턴트 응답은 산문과 코드를 하나의 문자열에 섞습니다. 전체를
패러프레이즈 모델에 넘기면 산문과 함께 코드도 재작성됩니다 — 어느 쪽이든 z-score가
떨어지므로 다운스트림에서는 아무것도 감지하지 못합니다.
따라서 응답은 재작성 전에 먼저 분할됩니다:
| 세그먼트 | 처리 |
| --- | --- |
| 산문 | 유니코드 정리 후 재작성 |
| 펜스 블록(``` 및 ~~~) | 보이지 않는 문자 및 양방향 문자 제거. **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` 대신 사용하면 동일하게 렌더링되는 두 개의 이름이 생성됩니다.```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":
purge 실행 후에 해당 줄은 if access_level != "user // Check if admin":으로 읽힙니다 —
"주석"은 처음부터 문자열 안에 있었습니다.
코드 경로가 의도적으로 수행하지 않는 세 가지, 일반 텍스트 경로가 세 가지를 모두 수행했고, 각각은 소스에서 버그이기 때문입니다:
"Hello"는
"Hello"가 되고, "office"는 "office"가 됩니다. 이는 프로그램이 비교하고 해시하고
전송하는 대상을 변경합니다.а를 접으면, 컴파일러가 현재
별개로 취급하는 두 식별자가 병합됩니다 — 조용히 동작을 변경합니다. 심각도는
혼합 스크립트 토큰 (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 키가 있는 글꼴의 제로 폭 공백은
2바이트 글리프 인덱스이지 리터럴 U+200B가 아닙니다 — 따라서 일반적인 재작성은
레이아웃을 정리하지 않고 손상시킬 것입니다. inspect는 개수를 보고하고, purge는
침묵하지 않고 경고를 출력합니다. 침묵은 "아무것도
없었다"로 읽히기 때문입니다. 이를 제거하려면 텍스트를 추출하고, 그 텍스트에 nullorigin purge를 실행한 다음,
PDF를 재생성하세요.
--strip-annotations는 사용 가능하지만 기본적으로 꺼져 있습니다. 어노테이션에는 링크와
양식 필드가 포함되며, 주석만 있는 것이 아니므로 이를 제거하면 문서 동작이 변경됩니다.
엠 대시, 컬리 따옴표, 그리고 줄임표는 일반적인 워드프로세서 출력입니다. 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에 경고로 보고됩니다. — 자동 대체(fallback)는
성공적인 정화(sanitization)와 동일하게 보일 것입니다.
---
## 📊 벤치마킹```bash
nullorigin build-datasets
nullorigin benchmark
모든 값은 즉석에서 측정됩니다. 텍스트는 KGWWatermarkEmbedder로 워터마킹되고, 실제 파이프라인을 통과한 후, 일치하는 탐지기로 다시 점수가 매겨집니다. 러너는 임계값이 충족되지 않으면 0이 아닌 종료 코드를 반환하고 그 이유를 설명합니다.
임계값 (프로젝트 지침 기준):
| 지표 | 목표 |
|---|---|
| 정화 후 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에서 다시 작성(구절당 약 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
Both image thresholds pass (SSIM ≥ 0.95, PSNR ≥ 36 dB). Numbers will vary with the model, hardware, and passage.
사실을 바꾸는 재작성은 워터마크 지표에서 충실한 재작성과 정확히 동일한 점수를 받습니다. 이 문제에 대한 명백한 검사는 작동하지 않으며, 덜 명백한 검사도 마찬가지입니다. 6가지 드리프트 사례와 충실한 대조군에 대해 측정했습니다:
어휘 중첩은 역전되어 있습니다. 의미를 파괴하는 모든 편집은 충실한 재작성보다 높은 점수를 받았습니다. 좋은 패러프레이즈는 원본과 n-gram을 거의 공유하지 않는 반면, 손상된 패러프레이즈는 거의 모든 n-gram을 공유하기 때문입니다.
임베딩 코사인은 이를 해결하지 못합니다. 6개의 손상된 사례 중 3개가 0.92 임계값을 통과합니다. "Alice paid Bob"과 "Bob paid Alice"는 동일한 bag of words이며 0.985를 기록합니다. "must not disable" → "must disable"은 0.947을 기록합니다. 문장 임베딩은 주제 관련성을 인코딩할 뿐, 진실을 인코딩하지 않습니다.
따라서 충실도는 두 계층에서 검사되며, 어느 것도 코사인은 아닙니다:
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-score가 낮아지고 *또한* 드리프트 레이트가 높아집니다. 벤치마크는 이 둘을 독립적인 검사가 아니라
함께 보고합니다.
### 기타 지표
* **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
구성된 모델이 pull되지 않았습니다. 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 exits 1 with pass_no_detection: FAIL
의도된 동작입니다. 재작성 백엔드에 연결할 수 없었으므로 유니코드 계층만 실행되었고 통계적 워터마크가 유지되었습니다. Ollama를 시작하거나, 의존성 없는 비교를 위해 백엔드를 lexical로 설정하세요.
semantic_check: INCONCLUSIVE
[metrics] 엑스트라가 없으면 예상되는 결과입니다. 메트릭 정직성을 참조하세요.
Error: Refusing to bind 0.0.0.0 without a telemetry token
의도적입니다. 로컬호스트를 넘어 배포하기를 참조하세요.
Multiple top-level packages discovered in a flat-layout
오래된 체크아웃을 사용 중입니다. pyproject.toml이 명시적 패키지 목록을 설정하므로 최신 버전을 pull하세요.
Async tests report UsageError about a missing async plugin
의도적입니다 — 플러그인이 없으면 pytest는 async def 테스트를 await하지 않고 통과한 것으로 보고합니다. pip install -e ".[dev]"를 실행하세요.
Docker: curl: (7) Failed to connect right after compose up
헬스체크에는 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 이미지 모드, 닫힌 형태(closed-form) 값과 무차별 대입
(brute-force) 참조 구현 모두에 대한 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 | 여기서는 검증 불가, 같은 이유로. |
| Compose v2를 포함한 Docker 20.10+ |
| case | lexical_f1 | embedding cosine | bidirectional 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 ✓ |