Universal AI Provenance & Watermark Sanitization Middleware Research artifact — for watermarking robustness evaluation only.
NullOrigin is a research artifact. It is published to support academic and independent study of watermarking robustness, and for no other purpose.
Watermarking schemes are security claims, and security claims are only meaningful once someone has tried to break them. The literature this project implements — Kirchenbauer et al. on KGW, Krishna et al. on paraphrase attacks, Boucher & Anderson on Trojan Source — exists because researchers published working attacks so that defenders could measure real robustness instead of assuming it. That is the tradition this repository belongs to.
Intended uses
- Measuring how much a watermark's detectability degrades under a stated attack
- Reproducing and extending published robustness results
- Evaluating provenance metadata leakage in your own documents before you share them
- Auditing source code for Trojan Source and homoglyph attacks (CVE-2021-42574, CVE-2021-42694)
- Teaching how these schemes work, and where they hold up
Not intended, and not supported
- Passing generated work off as your own where a person or institution has asked you not to
- Defeating provenance signals on content you intend to misrepresent
- Removing attribution from work that is not yours
- Circumventing any platform, licence, or academic-integrity policy
Nothing here is a technical control on how the code runs. It is a statement of the terms on which it is offered, and of what its author will and will not support. The software is provided "AS IS", without warranty of any kind — see LICENSE.
Read Scope and honest limitations before drawing any conclusion from a number this tool prints. Several of the schemes it targets cannot be verified against a public detector, and the README says so rather than implying otherwise.
- Research use only
- Scope and honest limitations
- Theoretical foundations
- Installation
- Quickstart
- Docker
- Deploying beyond localhost
- CLI
- Benchmarking
- Semantic fidelity
- Configuration
- Troubleshooting
- Architecture
- Testing
- Citing and reuse
- Project status
- References
- Legal notice & attribution
Read this before drawing conclusions from any number this tool prints.
| Layer | What it actually does |
|---|---|
| Invisible characters | Fully effective. Zero-width, bidi-control, variation-selector, and Unicode Tags-block payloads are removed completely, with a count reported. Cross-script homoglyph confusables (Cyrillic/Greek rendering as ASCII) are folded. |
| Document metadata (.docx) | Fully effective. Author, last editor, revision count, timestamps, template, and application version are cleared from docProps, with formatting preserved byte-for-byte. |
| C2PA / EXIF / XMP | Fully effective. The image is rebuilt from raw pixel samples into a fresh container, so signed JUMBF manifests and all metadata are gone. Verified by test against tagged fixtures. |
| KGW statistical watermark | Depends entirely on the rewrite backend. With no local model configured, the statistical watermark survives — the tool says so rather than implying otherwise. |
| SynthID-Text / SynthID-Image / Tree-Ring | Not verifiable here. These use private keys and proprietary decoders. NullOrigin applies the perturbations the literature describes, but no claim is made that they defeat the real detectors, because there is no public detector to measure against. |
| AudioSeal / SynthID-Audio | Not verifiable here, for the same reason. |
KGWStatisticalDetector is a mathematically faithful, self-consistent implementation
of the Kirchenbauer et al. green/red-list scheme over whitespace tokens. It is not a
decoder for any vendor's production watermark — those key on a private secret and the
model's own BPE vocabulary.
Its purpose is to make the benchmark real: KGWWatermarkEmbedder plants a genuine
watermark, the pipeline attacks it, and the matching detector measures the actual
reduction. That is a true measurement of the attack against this scheme. It does not
transfer to a vendor's watermark.
The vocabulary
into a green list
Detection counts green hits. Under
with
Why paraphrasing attacks it: the watermark lives entirely in local n-gram transitions. Rewriting the surface form with an unwatermarked model reseeds every position. This is the standard robustness attack in the watermarking literature.
Why length matters:
- C2PA / JUMBF: signed manifests in JPEG
APP11segments, PNGtEXt/iTXtchunks, or WebP/AVIFc2paboxes. Because the signature covers the pixel data, re-encoding from a bare sample buffer removes it without parsing JUMBF at all. - SynthID-Image: shifts in DCT/DWT coefficients. Attacked by soft-thresholding the high-frequency detail subbands.
- Tree-Ring: a circularly symmetric pattern in the initial diffusion latent, surviving as coherent structure in the Fourier phase. Attacked by randomizing phase in mid-frequency annuli while preserving magnitude — which is why the image stays visually identical.
Sub-threshold phase modulation and low-amplitude spectral additions. Attacked by phase randomization above the speech fundamental, band-stop notch shifting in non-critical bands, and psychoacoustic re-quantization.
| Python | 3.10, 3.11, or 3.12 |
| OS | Linux, macOS (Intel and Apple Silicon), Windows via WSL2 |
| Optional | Ollama or any OpenAI-compatible server — required for text de-watermarking |
| Optional | Docker 20.10+ with 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 .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]"Without
[nli], the fidelity gate runs on invariants alone — still a real check, but blind to role swaps. See Semantic fidelity.
nullorigin --version
nullorigin --help
pytest -q # requires the [dev] extraText de-watermarking needs an unwatermarked local model. Without one, NullOrigin removes invisible characters but leaves the statistical watermark intact — and says so.
ollama serve # in a separate terminal
ollama pull llama3.2:3b # or any instruct model you preferUsing a different model? Point NullOrigin at it:
export NULLORIGIN_PARAPHRASER_MODEL=qwen3:4b
export NULLORIGIN_PARAPHRASER_TIMEOUT=900 # reasoning models are slownullorigin runNullOrigin 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/healthfrom 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:
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:
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) and Gemini (/v1beta/models/...) are handled the same way. The
x-nullorigin-provider header selects the upstream and is stripped before forwarding;
your auth headers pass through untouched.
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/healthThe Compose stack runs NullOrigin plus an Ollama sidecar on a private bridge network.
The proxy container binds 0.0.0.0 — correct inside a container — and only port 8080 is
published to your host.
Standalone image:
docker build -t nullorigin:1.0.0 .
docker run -d -p 8080:8080 \
-e NULLORIGIN_PARAPHRASER_BACKEND=none \
nullorigin:1.0.0Useful commands:
docker compose logs -f nullorigin
docker compose down # stop
docker compose down -v # stop and delete the Ollama model volumeNullOrigin defaults to 127.0.0.1 and refuses to bind a public interface without a
telemetry token. It relays your upstream API credentials, so this is deliberate:
$ 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-bindTo expose it properly:
export NULLORIGIN_TELEMETRY_TOKEN=$(openssl rand -hex 32)
nullorigin run --host 0.0.0.0 --port 8080then put it behind nginx, Caddy, or Traefik providing TLS termination, rate limiting, and an auth layer.
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
Authorizationandx-api-keyverbatim 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 imrakibul@gmail.com with [NullOrigin Security] in
the subject.
Telemetry with a token set:
curl -H "X-NullOrigin-Token: $NULLORIGIN_TELEMETRY_TOKEN" http://localhost:8080/telemetry/health is never gated, so container probes keep working.
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...]| Kind | Extensions | Notes |
|---|---|---|
| Images | .png .jpg .jpeg .jfif .webp .tif .tiff .bmp .gif .ico .avif .jp2 |
All PIL modes (RGB, RGBA, L, LA, P, 1, I;16, CMYK, YCbCr). Animated GIF/WebP and multipage TIFF keep every frame and their timing. Images under 64px keep exact dimensions. |
| Audio | .wav .wave |
8/16/32-bit int, 32-bit float; mono through multichannel; any sample rate. Zero-length files round-trip. |
| Documents | .docx .docm .dotx .pptx .pptm .xlsx .xlsm |
All three OOXML dialects. Text runs sanitized across body, headers, footers, footnotes, notes, and shared strings; docProps metadata cleared; every other part copied byte-for-byte. |
.pdf |
/Info dictionary, XMP packet, embedded attachments, and JavaScript removed; pages, text, and geometry preserved. Password-protected files are refused. See the caveat below. |
|
| Source code | .py .js .ts .go .rs .java .c .cpp .rb .php .sh .sql + 50 more |
Trojan Source and homoglyph scanning. No NFKC, no paraphrasing — see below. |
| Text | anything else decodable | UTF-8, UTF-8 BOM, UTF-16, UTF-32, CP1252, Latin-1 — auto-detected and written back in the same encoding. |
Everything else is refused with specific guidance rather than read as UTF-8 and
corrupted — .mp3 points at ffmpeg -i in.mp3 out.wav, legacy .doc/.ppt/.xls at
re-saving as OOXML. A refused file never produces output.
Verified against a 69-file corpus spanning every format above: 59 processed correctly, 10 refused cleanly, zero crashes, zero corrupted outputs.
Assistant replies mix prose and code in one string. Handing the whole thing to a paraphrasing model rewrites the code along with the prose — and the z-score drops either way, so nothing downstream notices.
Responses are therefore segmented before anything is rewritten:
| Segment | Treatment |
|---|---|
| Prose | Unicode-cleaned, then rewritten |
| Fenced blocks (``` and ~~~) | Invisible and bidi characters stripped. No NFKC, never rewritten. |
Inline `code` spans |
Same |
This holds on the streaming path too, where a fence opens in one delta and closes several deltas later. A delta that straddles the boundary is split per line, so the closing ``` and the prose after it are treated differently. An unclosed fence fails safe: the remainder is protected rather than rewritten.
Disable with text.protect_code_blocks: false if you want the old behaviour.
There is no watermark in AI-generated source code. No provider watermarks code output, and no public detector exists. Anyone claiming to strip one is selling you something.
What source code does have is a real, published attack surface:
- Trojan Source (CVE-2021-42574, Boucher & Anderson 2021) — bidirectional control characters reorder how code is displayed without changing how it compiles. A reviewer approves one program; the compiler builds another.
- Homoglyph identifiers (CVE-2021-42694)
— Cyrillic
аfor Latinacreates two names that render identically.
$ 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":After purging, the line reads if access_level != "user // Check if admin": — the
"comment" was inside the string all along.
Three things the code path deliberately does not do, because the generic text path did all three and each is a bug on source:
- No NFKC normalization. It rewrites string literal values:
"Hello"became"Hello","office"became"office". That changes what a program compares, hashes, and transmits. - No paraphrasing. Sending code to a rewriting model produces code that does not run.
- Homoglyphs are reported, not folded. Folding a Cyrillic
аmerges two identifiers the compiler currently treats as distinct — silently changing behavior. Severity is MEDIUM only for mixed-script tokens (totаl), the actual attack signature; a word written entirely in another script is ordinary foreign text and scores INFO. Opt in with--fold-homoglyphs-in-codeonce you have reviewed them.
inspect --json emits per-finding severity, line, column, and codepoint, so it drops
into CI as a pre-commit or PR gate.
Removed, verifiably: the /Info dictionary (Author, Title, Subject, Keywords,
Creator, Producer, CreationDate, ModDate), the XMP packet in /Root/Metadata, embedded
file attachments, and document-level JavaScript. Pages, text, and page geometry are
preserved exactly; the operation is idempotent and byte-stable.
Detected but NOT removed: invisible characters inside page content streams. PDF
draws text glyph by glyph through a font-specific encoding — a zero-width space in a
CID-keyed font is a two-byte glyph index, not a literal U+200B — so a generic rewrite
would corrupt the layout rather than clean it. inspect reports the count; purge
prints a warning rather than staying silent, because silence would read as "there were
none". To remove them, extract the text, run nullorigin purge on it, and regenerate
the PDF.
--strip-annotations is available but off by default: annotations include links and
form fields, not only comments, so removing them changes how the document behaves.
Em dashes, curly quotes, and ellipses are ordinary word-processor output. NullOrigin
preserves them by default and reports them separately from genuine findings, because
flattening them degrades a document without sanitizing anything. Use
--flatten-typography if you specifically want ASCII output.
Cross-script confusables are different — a Cyrillic о mid-word in English text has no
legitimate use — and those are folded by default.
--verify reports before/after measurements rather than asserting success:
$ 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 -> FalseIf the backend is unreachable, that is reported as a warning on stderr — a silent fallback would look identical to a successful sanitization.
nullorigin build-datasets
nullorigin benchmarkEvery value is measured on the spot: text is watermarked with KGWWatermarkEmbedder, run
through the real pipeline, and re-scored with the matching detector. The runner exits
non-zero when thresholds are not met and explains why.
Thresholds (from the project directive):
| Metric | Target |
|---|---|
| Post-sanitization z-score | |
| Semantic similarity | |
| Image SSIM | |
| Image PSNR |
|
Full text section over datasets/text/watermarked_kgw.json, rewritten through Ollama
(qwen3:4b) on an M-series MacBook (~150 s per passage):
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)
Every watermarked sample crossed from detected to undetected. Note kgw_003 at
z = 1.271 — under threshold but the closest to it, which is the honest shape of this
attack: it is statistical, not a guarantee.
Media, measured across the image fixtures:
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.
A rewrite that changes a fact scores exactly like a faithful one on the watermark metric. The obvious check for this does not work, and neither does the less obvious one. Measured on six drift cases plus a faithful control:
| case | lexical_f1 |
embedding cosine | bidirectional NLI |
|---|---|---|---|
| negation dropped | 0.70 | 0.77 ✓ | 0.000 ✓ |
| number 5 → 50 | 0.82 | 0.81 ✓ | 0.000 ✓ |
| entity/role swap | 0.81 | 0.985 ✗ | 0.000 ✓ |
| quantifier all → some | 0.88 | 0.91 ✓ | 0.000 ✓ |
| hedge removed | 0.27 | 0.953 ✗ | 0.011 ✓ |
| "must not" → "must" | 0.83 | 0.947 ✗ | 0.000 ✓ |
| faithful rewrite | 0.33 | 0.931 ✓ | 0.998 ✓ |
Lexical overlap is inverted. Every meaning-destroying edit scored higher than the faithful rewrite, because a good paraphrase shares few n-grams with its source while a corrupted one shares nearly all of them.
Embedding cosine does not fix it. Three of six corrupted cases pass a 0.92 threshold.
"Alice paid Bob" and "Bob paid Alice" are the same bag of words and score 0.985;
"must not disable" → "must disable" scores 0.947. Sentence embeddings encode topical
relatedness, not truth.
So fidelity is checked in two layers, and neither is cosine:
- Invariants — numbers, negation count, modal strength, quantifier strength, URLs,
emails, identifiers, named entities. Deterministic, dependency-free, and explainable
(
negation count changed: 1 → 0). Modals and quantifiers are compared by meaning class, somay → mightpasses andmay → mustfails. Blind to role swaps where every entity survives. - Bidirectional entailment — meaning is preserved iff each text entails the other.
Catches what counting cannot, including role swaps, and the asymmetry is diagnostic
(hedge removal scored a⇒b 0.991, b⇒a 0.011). Requires
nullorigin[nli]; without it the limitation is reported, not hidden.
Measuring drift after the fact does not help if the damaged text has already been
returned. A failed check retries at a lower temperature — drift is temperature-driven
— and after the retry budget returns the original, with ok=False and the reason.
text:
fidelity:
enabled: true
max_retries: 2
temperature_step: 0.25
use_nli: true
nli_threshold: 0.5This also means the attack and the risk share one dial: turning up temperature lowers the z-score and raises the drift rate. The benchmark reports them together rather than as independent checks.
- Perplexity — true GPT-2 PPL with
torch+transformers, otherwiseunigram_entropy_proxy, flagged approximate and not comparable to published PPL. - Cosine similarity is still reported as
mean_cosine_or_lexical, for reference only. It is no longer a pass/fail gate, for the reasons in the table above.
Resolution order, lowest to highest precedence:
- Built-in defaults
nullorigin.yaml(searched in./,../,/app/, or$NULLORIGIN_CONFIG)NULLORIGIN_*environment variables- Explicit CLI flags
| Setting | Default | Notes |
|---|---|---|
proxy.host |
127.0.0.1 |
Loopback. A public bind is refused without a telemetry token. |
proxy.port |
8080 |
|
proxy.default_provider |
openai |
Used when no x-nullorigin-provider header is sent. |
proxy.telemetry_token |
"" |
Guards /telemetry and /telemetry/reset. |
proxy.max_request_bytes |
104857600 |
100 MiB; larger bodies get 413. |
text.paraphraser.backend |
ollama |
none | ollama | openai_compatible | llama_cpp | lexical. none leaves the statistical watermark intact. lexical needs no model but is a far weaker attack. |
text.clean_unicode |
true |
Zero-width and Tags-block removal. |
text.fold_homoglyphs |
true |
Cyrillic/Greek confusables to ASCII. |
text.stream_window_tokens |
40 |
Deltas buffered before a streaming span is rewritten. |
media.crop_mode |
trim |
trim shifts coordinates without resampling; resample restores exact dimensions but costs roughly SSIM 0.81 / PSNR 31 dB even at a 0.5% crop; none disables the geometric pass. |
audio.low_cut_hz |
800.0 |
Phase below this is preserved for intelligibility. |
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_AUDIOmodel 'llama3.2:3b' not found
The configured model is not pulled. Run ollama list to see what you have, then either
ollama pull llama3.2:3b or set NULLORIGIN_PARAPHRASER_MODEL to a model you already
have.
WARNING: ollama backend unavailable (ReadTimeout)
The rewrite exceeded text.paraphraser.timeout_seconds (default 120 s). Reasoning models
such as qwen3 routinely take 150 s+ per paragraph on CPU. Raise it:
export NULLORIGIN_PARAPHRASER_TIMEOUT=900, or use a smaller instruct model.
nullorigin benchmark exits 1 with pass_no_detection: FAIL
Working as intended. No rewrite backend was reachable, so only the unicode layer ran and
the statistical watermark survived. Start Ollama, or set the backend to lexical for a
dependency-free comparison.
semantic_check: INCONCLUSIVE
Expected without the [metrics] extra. See Metric honesty.
Error: Refusing to bind 0.0.0.0 without a telemetry token
Intentional. See Deploying beyond localhost.
Multiple top-level packages discovered in a flat-layout
You are on an old checkout. pyproject.toml sets an explicit package list; pull latest.
Async tests report UsageError about a missing async plugin
Deliberate — without one, pytest reports async def tests as passing without awaiting
them. pip install -e ".[dev]".
Docker: curl: (7) Failed to connect right after compose up
The healthcheck has a 10 s start period. Wait, then check 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
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 -q384 tests. The suite covers the SSE frame parser against adversarial chunk boundaries, proxy streaming lifecycle, every PIL image mode, SSIM against both a closed-form value and a brute-force reference implementation, and dataset watermark detectability.
Async tests fail loudly if no async plugin is installed rather than silently skipping.
Licensed under Apache-2.0, which permits use, modification, and redistribution provided the copyright notice and attribution to Muhammad Rakibul Islam are retained. See LICENSE and NOTICE.
If this work supports a publication, please cite it as:
@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}
}This repository is published as a finished research artifact and is not accepting pull requests. You are free to fork it under the terms of the licence. Questions and findings are welcome by email at imrakibul@gmail.com.
Version 1.0.0. The suite is complete against its specification and fully tested, with these known boundaries:
- Effectiveness against SynthID, Tree-Ring, and AudioSeal is unverifiable here — no public detector exists to measure it. The perturbations are implemented; the claim is not made.
- Semantic drift is gated by invariants plus optional NLI. Without
[nli], role swaps that preserve every entity are undetectable, and the report says so. - Latency with a local reasoning model (~150 s per passage) makes the text path unsuitable for interactive use. A smaller instruct model is much faster.
- No TLS and no request authentication — by design; run it behind a reverse proxy.
See CHANGELOG.md for release history.
- Kirchenbauer et al., A Watermark for Large Language Models (2023) — the KGW scheme
- Dathathri et al., Scalable watermarking for identifying large language model outputs (Nature, 2024) — SynthID-Text
- Wen et al., Tree-Rings Watermarks: Invisible Fingerprints for Diffusion Images (2023)
- San Roman et al., Proactive Detection of Voice Cloning with Localized Watermarking (2024) — AudioSeal
- Krishna et al., Paraphrasing evades detectors of AI-generated text (2023)
- Wang et al., Image Quality Assessment: From Error Visibility to Structural Similarity (2004) — SSIM
- C2PA Specification
- Author & Creator: Muhammad Rakibul Islam
- Contact: imrakibul@gmail.com
- License: Apache License, Version 2.0
This repository is a research-grade artifact released for statistical research, privacy evaluation, watermarking robustness benchmarking, and cryptographic resilience testing.
Copyright 2026 Muhammad Rakibul Islam <imrakibul@gmail.com>
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.
NO WARRANTIES. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.