
Toolkit per la robustezza dei watermark AI, solo a scopo di ricerca: un reverse proxy locale rimuove C2PA/EXIF/XMP, Unicode, steganografia di immagini/audio, metadati OOXML/PDF e analizza Trojan Source.
Middleware universale per la provenienza AI e la sanificazione dei watermark Artefatto di ricerca — solo per la valutazione della robustezza del watermarking.
NullOrigin è un artefatto di ricerca. Viene pubblicato per supportare lo studio accademico e indipendente della robustezza del watermarking, e per nessun altro scopo.
Gli schemi di watermarking sono affermazioni di sicurezza, e le affermazioni di sicurezza sono significative solo dopo che qualcuno ha provato a romperle. La letteratura che questo progetto implementa — Kirchenbauer et al. su KGW, Krishna et al. sugli attacchi di parafrasi, Boucher & Anderson su Trojan Source — esiste perché i ricercatori hanno pubblicato attacchi funzionanti affinché i difensori potessero misurare la robustezza reale invece di assumerla. È questa la tradizione a cui appartiene questo repository.
Usi previsti
Non previsto e non supportato
Nulla di quanto scritto qui è un controllo tecnico su come il codice viene eseguito. È una dichiarazione dei termini con cui viene offerto e di ciò che il suo autore supporterà o meno. Il software è fornito "AS IS", senza garanzie di alcun tipo — vedi LICENSE.
Leggi Scopo e limiti onesti prima di trarre qualsiasi conclusione da un numero stampato da questo strumento. Diversi degli schemi presi di mira non possono essere verificati con un rilevatore pubblico, e il README lo dice esplicitamente piuttosto che suggerire il contrario.
Leggi questo prima di trarre conclusioni da qualsiasi numero stampato da questo strumento.
KGWStatisticalDetector è un'implementazione matematicamente fedele e autoconsistente
dello schema green/red-list di Kirchenbauer et al. sui token di spazio bianco. Non è un
decoder per il watermark di produzione di alcun fornitore — questi si basano su un segreto privato e
il vocabolario BPE del modello stesso.
Il suo scopo è rendere il benchmark reale: KGWWatermarkEmbedder inserisce un watermark
genuino, la pipeline lo attacca e il rilevatore corrispondente misura la riduzione
effettiva. Questa è una misurazione vera dell'attacco contro questo schema. Non si
trasferisce al watermark di un fornitore.
Il vocabolario $V$ viene partizionato a ogni passo $t$ da un hash inizializzato sul contesto precedente:
$$s_t = \text{Hash}(w_{t-k}, \dots, w_{t-1})$$
in una lista verde $G_t$ di dimensione $\gamma|V|$ e una lista rossa $R_t$. Un bias $\delta > 0$ viene aggiunto ai logit verdi:
$$\tilde{l}{t,v} = \begin{cases} l{t,v} + \delta, & v \in G_t \\ l_{t,v}, & v \in R_t \end{cases}$$
La rilevazione conta i successi verdi. Sotto $H_0$ sono $\text{Binomial}(T, \gamma)$, quindi:
$$z = \frac{|S_G| - \gamma T}{\sqrt{T\gamma(1-\gamma)}}$$
con $z > 4.0$ ($p < 3\times10^{-5}$) segnalato come sintetico.
Perché la parafrasi lo attacca: il watermark vive interamente nelle transizioni n-gram locali. Riscrivere la forma superficiale con un modello non marcato re-inizializza ogni posizione. Questo è l'attacco di robustezza standard nella letteratura sul watermarking.
Perché la lunghezza è importante: $z$ cresce come $\sqrt{T}$. Un passaggio di 100 token a una frazione verde di 0,70 raggiunge solo $z \approx 3,9$ — sotto la soglia. La rilevazione richiede qualche centinaio di token, e così anche i fixture di benchmark significativi.
APP11, chunk PNG tEXt/iTXt,
o box c2pa WebP/AVIF. Poiché la firma copre i dati dei pixel, la ricodifica
da un buffer di campioni nudo lo rimuove senza dover analizzare affatto JUMBF.Modulazione di fase sotto-soglia e aggiunte spettrali a bassa ampiezza. Attaccate tramite randomizzazione della fase sopra la fondamentale del parlato, spostamento di notch band-stop in bande non critiche e ri-quantizzazione psicoacustica.
| Python | 3.10, 3.11 o 3.12 |
| Sistema operativo | Linux, macOS (Intel e Apple Silicon), Windows tramite WSL2 |
| Opzionale | Ollama o qualsiasi server compatibile con 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 .
### Extra opzionali```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]"
Senza
[nli], il gate di fedeltà si basa esclusivamente sugli invarianti — un controllo comunque reale, ma cieco agli scambi di ruolo. Vedi Fedeltà semantica.
nullorigin --version nullorigin --help pytest -q # requires the [dev] extra
---
## 🚀 Avvio rapido
### 1. Configura un modello di riscrittura locale
La rimozione della filigrana dal testo richiede un modello locale senza filigrana. Senza uno, NullOrigin rimuove
i caratteri invisibili ma **lascia intatta la filigrana statistica** — e lo dichiara.```bash
ollama serve # in a separate terminal
ollama pull llama3.2:3b # or any instruct model you prefer
Stai usando un modello diverso? Punta NullOrigin su di esso:```bash export NULLORIGIN_PARAPHRASER_MODEL=qwen3:4b export NULLORIGIN_PARAPHRASER_TIMEOUT=900 # reasoning models are slow
### 2. Avvia il proxy```bash
nullorigin run
Please provide the Markdown content to translate.```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. Punta il tuo client verso di esso```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) e Gemini (/v1beta/models/...) sono gestiti allo stesso modo. L'header x-nullorigin-provider seleziona l'upstream e viene rimosso prima dell'inoltro; i tuoi header di autenticazione passano senza modifiche.
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
Lo stack Compose esegue NullOrigin più un sidecar Ollama su una rete bridge privata.
Il container proxy si lega a `0.0.0.0` — corretto all'interno di un container — e solo la porta 8080 è
pubblicata sul tuo host.
Immagine standalone:```bash
docker build -t nullorigin:1.0.0 .
docker run -d -p 8080:8080 \
-e NULLORIGIN_PARAPHRASER_BACKEND=none \
nullorigin:1.0.0
Comandi utili:```bash docker compose logs -f nullorigin docker compose down # stop docker compose down -v # stop and delete the Ollama model volume
---
## 🔒 Distribuzione oltre localhost
**NullOrigin per impostazione predefinita usa `127.0.0.1` e rifiuta di associarsi a un'interfaccia pubblica senza un
token di telemetria.** Inoltra le credenziali API upstream, quindi questa è una scelta deliberata:```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
Per esporlo correttamente:```bash export NULLORIGIN_TELEMETRY_TOKEN=$(openssl rand -hex 32) nullorigin run --host 0.0.0.0 --port 8080
poi mettilo dietro nginx, Caddy o Traefik che forniscono **terminazione TLS**, **limitazione della frequenza** e un **livello di autenticazione**.
### Modello di minaccia
NullOrigin è un **proxy inverso locale che inoltra le credenziali API del tuo upstream**. Questo singolo fatto determina la sua postura di sicurezza.
| Controllo | Predefinito | Perché |
| --- | --- | --- |
| Indirizzo di bind | `127.0.0.1` | Solo loopback; un bind pubblico viene rifiutato a meno che non sia impostato un token di telemetria o non venga passato `--allow-public-bind`. |
| `/telemetry`, `/telemetry/reset` | Aperti su loopback | Protetti da `X-NullOrigin-Token`, confrontato a tempo costante, quando `proxy.telemetry_token` è impostato. |
| `/health` | Sempre aperto | I probe dei container ne hanno bisogno; espone versione e motori abilitati, nessun segreto. |
| Dimensione del corpo della richiesta | 100 MiB | Il proxy bufferizza i body per inoltrarli; input più grandi vengono rifiutati con `413`. |
| Buffer SSE | 1 MiB | Un upstream che non termina mai un frame viene svuotato, non bufferizzato indefinitamente. |
| Utente del container | non-root | Il proxy non necessita di privilegi elevati. |
Limitazioni note, per scelta progettuale e non difetti:
* **Niente TLS.** Inoltra `Authorization` e `x-api-key` così come sono su HTTP in chiaro. Mettilo dietro un reverse proxy che termina HTTPS su qualsiasi rete non affidabile.
* **Nessuna autenticazione sul percorso del proxy.** Chiunque possa raggiungere la porta può usarlo come proxy, con le proprie credenziali — NullOrigin non memorizza né inietta chiavi.
* **Nessuna limitazione della frequenza.** Applicala al reverse proxy.
* **Le credenziali non vengono mai persistite.** Nessuna chiave API viene scritta su disco o nei log; la telemetria conta solo richieste ed eventi di sanitizzazione.
* La verifica TLS dell'upstream resta attiva e i redirect non vengono seguiti.
Per segnalare un problema di sicurezza, invia un'email a **[email protected]** con `[NullOrigin Security]` nell'oggetto.
Telemetria con token impostato:```bash
curl -H "X-NullOrigin-Token: $NULLORIGIN_TELEMETRY_TOKEN" http://localhost:8080/telemetry
/health non è mai soggetto a gate, quindi i probe dei container continuano a funzionare.
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...]
### Formati supportati
| Tipo | Estensioni | Note |
| --- | --- | --- |
| **Immagini** | `.png` `.jpg` `.jpeg` `.jfif` `.webp` `.tif` `.tiff` `.bmp` `.gif` `.ico` `.avif` `.jp2` | Tutte le modalità PIL (RGB, RGBA, L, LA, P, 1, I;16, CMYK, YCbCr). GIF/WebP animati e TIFF multipagina conservano ogni fotogramma e i relativi tempi. Le immagini sotto i 64px mantengono le dimensioni esatte. |
| **Audio** | `.wav` `.wave` | int a 8/16/32 bit, float a 32 bit; da mono a multicanale; qualsiasi frequenza di campionamento. I file di lunghezza zero vengono mantenuti invariati. |
| **Documenti** | `.docx` `.docm` `.dotx` `.pptx` `.pptm` `.xlsx` `.xlsm` | Tutti e tre i dialetti OOXML. Le sequenze di testo vengono sanificate in corpo, intestazioni, piè di pagina, note a piè di pagina, note e stringhe condivise; i metadati `docProps` vengono cancellati; ogni altra parte viene copiata byte per byte. |
| **PDF** | `.pdf` | Dizionario `/Info`, pacchetto XMP, allegati incorporati e JavaScript rimossi; pagine, testo e geometria preservati. I file protetti da password vengono rifiutati. Vedere l'avvertenza di seguito. |
| **Codice sorgente** | `.py` `.js` `.ts` `.go` `.rs` `.java` `.c` `.cpp` `.rb` `.php` `.sh` `.sql` + altri 50 | Scansione Trojan Source e omoglifi. **Niente NFKC, niente parafrasi** — vedere sotto. |
| **Testo** | qualsiasi altra cosa decodificabile | UTF-8, UTF-8 BOM, UTF-16, UTF-32, CP1252, Latin-1 — rilevati automaticamente e **riscritti nella stessa codifica**. |
Tutto il resto viene **rifiutato con indicazioni specifiche** piuttosto che letto come UTF-8 e corrotto — `.mp3` rimanda a `ffmpeg -i in.mp3 out.wav`, i legacy `.doc`/`.ppt`/`.xls` a un nuovo salvataggio come OOXML. Un file rifiutato non produce mai output.
Verificato su un corpus di 69 file che copre ogni formato sopra: **59 elaborati correttamente, 10 rifiutati in modo pulito, zero crash, zero output corrotti.**
### Il codice è tenuto fuori dal riscrittore
Le risposte dell'assistente mescolano prosa e codice in un'unica stringa. Passare l'intero contenuto a un modello di parafrasi riscrive il codice insieme alla prosa — e lo z-score diminuisce in entrambi i casi, quindi nulla a valle se ne accorge.
Le risposte vengono quindi segmentate prima che qualsiasi cosa venga riscritta:
| Segmento | Trattamento |
| --- | --- |
| Prosa | Pulita a livello Unicode, poi riscritta |
| Blocchi delimitati (``` e ~~~) | Caratteri invisibili e bidi rimossi. **Niente NFKC, mai riscritti.** |
| Span di codice inline `` `code` `` | Lo stesso |
Questo vale anche nel percorso di streaming, dove un delimitatore si apre in un delta e si chiude parecchi delta dopo. Un delta che attraversa il confine viene diviso per riga, così la ``` di chiusura e la prosa successiva vengono trattate in modo diverso. Un delimitatore non chiuso fallisce in modo sicuro: la parte rimanente viene protetta, non riscritta.
Disattivare con `text.protect_code_blocks: false` se si vuole il vecchio comportamento.
### File di codice sorgente: una scansione di sicurezza, non una rimozione di filigrane
**Non esiste alcuna filigrana nel codice sorgente generato dall'IA.** Nessun fornitore filigrana l'output del codice e non esiste alcun rilevatore pubblico. Chiunque affermi di rimuoverne una ti sta vendendo qualcosa.
Ciò che il codice sorgente *ha* davvero è una superficie d'attacco reale e pubblicata:
* **Trojan Source** ([CVE-2021-42574](https://nvd.nist.gov/vuln/detail/CVE-2021-42574), Boucher & Anderson 2021) — i caratteri di controllo bidirezionali riordinano il modo in cui il codice viene *visualizzato* senza cambiare come *compila*. Un revisore approva un programma; il compilatore ne costruisce un altro.
* **Identificatori omoglifi** ([CVE-2021-42694](https://nvd.nist.gov/vuln/detail/CVE-2021-42694)) — la `а` cirillica al posto della `a` latina crea due nomi che vengono resi in modo identico.```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":
Dopo la purga, la riga recita if access_level != "user // Check if admin": — il
"commento" era dentro la stringa fin dall'inizio.
Tre cose che il percorso del codice deliberatamente non fa, perché il percorso testuale generico faceva tutte e tre e ciascuna è un bug sul sorgente:
"Hello" diventa
"Hello", "office" diventa "office". Questo cambia ciò che un programma confronta, sottopone a hash
e trasmette.а cirillica fonde due identificatori
che il compilatore attualmente tratta come distinti — cambiando silenziosamente il comportamento. La gravità è
MEDIUM solo per token a script misti (totаl), la firma effettiva dell'attacco; una parola
scritta interamente in un altro script è testo straniero ordinario e riceve INFO. Puoi attivare l'opzione
con --fold-homoglyphs-in-code una volta che li hai esaminati.inspect --json emette per ogni riscontro gravità, riga, colonna e codepoint, quindi si integra
nella CI come gate pre-commit o PR.
Rimosso, in modo verificabile: il dizionario /Info (Author, Title, Subject, Keywords,
Creator, Producer, CreationDate, ModDate), il pacchetto XMP in /Root/Metadata, gli allegati
di file incorporati e JavaScript a livello di documento. Pagine, testo e geometria di pagina vengono
preservati esattamente; l'operazione è idempotente e stabile a livello di byte.
Rilevati ma NON rimossi: i caratteri invisibili all'interno dei flussi di contenuto delle pagine.
Il PDF disegna il testo glifo per glifo tramite una codifica specifica del font — uno spazio a larghezza
zero in un font CID-keyed è un indice di glifo a due byte, non un U+200B letterale — quindi una
riscrittura generica corromperebbe il layout invece di pulirlo. inspect riporta il conteggio; purge
stampa un avviso invece di restare in silenzio, perché il silenzio potrebbe essere interpretato come "non ce n'erano".
Per rimuoverli, estrai il testo, esegui nullorigin purge su di esso e rigenera il PDF.
--strip-annotations è disponibile ma disattivata per impostazione predefinita: le annotazioni includono
collegamenti e campi modulo, non solo commenti, quindi rimuoverle cambia il comportamento del documento.
Trattini em, virgolette ricurve ed ellissi sono normale output di un word processor. NullOrigin
li preserva per impostazione predefinita e li segnala separatamente dai riscontri reali, perché
appiattirli degrada un documento senza sanificare nulla. Usa
--flatten-typography se vuoi specificamente un output ASCII.
I confondibili cross-script sono diversi — una о cirillica a metà parola in testo inglese non ha
alcun uso legittimo — e questi vengono ripiegati per impostazione predefinita.
--verify riporta le misurazioni prima/dopo invece di affermare il successo:```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
Se il backend non è raggiungibile, viene segnalato un avviso su stderr — un fallback silenzioso sembrerebbe identico a una sanificazione riuscita.
---
## 📊 Benchmarking```bash
nullorigin build-datasets
nullorigin benchmark
Ogni valore viene misurato sul posto: il testo viene marcato con KGWWatermarkEmbedder, eseguito attraverso la pipeline reale e rivalutato con il detector corrispondente. Il runner termina con codice non zero quando le soglie non vengono soddisfatte e ne spiega il motivo.
Soglie (dalla direttiva del progetto):
| Metrica | Obiettivo |
|---|---|
| Z-score post-sanitizzazione | $\lvert z\rvert \le 1.5$ |
| Similarità semantica | $\ge 0.92$ |
| SSIM dell'immagine | $\ge 0.95$ |
| PSNR dell'immagine | $\ge 36$ dB |
Sezione di testo completo su datasets/text/watermarked_kgw.json, riscritto tramite Ollama (qwen3:4b) su un MacBook serie M (~150 s per passaggio):```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)
Ogni campione filigranato è passato da rilevato a non rilevato. Nota `kgw_003` a
z = 1.271 — sotto la soglia ma il più vicino ad essa, che è la forma onesta di questo
attacco: è statistico, non una garanzia.
Media, misurata sulle immagini di test:```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
Entrambe le soglie delle immagini vengono superate (SSIM ≥ 0.95, PSNR ≥ 36 dB). I numeri varieranno con il modello, l'hardware e il passaggio.
Una riscrittura che cambia un fatto ottiene un punteggio identico a una fedele sulla metrica del watermark. Il controllo ovvio per questo non funziona, e non funziona nemmeno quello meno ovvio. Misurato su sei casi di deriva più un controllo fedele:
La sovrapposizione lessicale è invertita. Ogni modifica che distrugge il significato ha ottenuto un punteggio più alto della riscrittura fedele, perché una buona parafrasi condivide pochi n-grammi con la sua fonte, mentre una corrotta ne condivide quasi tutti.
Il coseno di embedding non lo risolve. Tre dei sei casi corrotti superano una soglia di 0.92.
"Alice paid Bob" e "Bob paid Alice" sono la stessa bag of words e ottengono 0.985;
"must not disable" → "must disable" ottiene 0.947. Gli embedding di frase codificano la relazione
tematica, non la verità.
Quindi la fedeltà è verificata su due livelli, e nessuno dei due è il coseno:
negation count changed: 1 → 0). I modali e i quantificatori sono confrontati per classe di
significato, quindi may → might supera e may → must fallisce. Insensibile agli scambi di ruolo in cui
ogni entità sopravvive.nullorigin[nli]; senza di esso
il limite viene riportato, non nascosto.Misurare la deriva dopo il fatto non aiuta se il testo danneggiato è già stato
restituito. Un controllo fallito riprova a una temperatura più bassa — la deriva è guidata dalla temperatura
— e dopo il budget di tentativi restituisce l'originale, con ok=False e il motivo.```yaml
text:
fidelity:
enabled: true
max_retries: 2
temperature_step: 0.25
use_nli: true
nli_threshold: 0.5
Questo significa anche che attacco e rischio condividono un unico controllo: alzare la temperatura abbassa lo
z-score *e* aumenta il tasso di drift. Il benchmark li riporta insieme, piuttosto che come
controlli indipendenti.
### Altre metriche
* **Perplexity** — vera PPL GPT-2 con `torch` + `transformers`, altrimenti
`unigram_entropy_proxy`, contrassegnata come approssimativa e **non** confrontabile con la PPL pubblicata.
* **Similarità coseno** viene ancora riportata come `mean_cosine_or_lexical`, solo come riferimento.
Non è più un criterio pass/fail, per i motivi indicati nella tabella sopra.
## ⚙️ Configurazione
Ordine di risoluzione, dalla precedenza più bassa alla più alta:
1. Valori predefiniti integrati
2. `nullorigin.yaml` (cercato in `./`, `../`, `/app/` o `$NULLORIGIN_CONFIG`)
3. Variabili d'ambiente `NULLORIGIN_*`
4. Flag CLI espliciti
### Impostazioni principali
| Impostazione | Predefinito | Note |
| --- | --- | --- |
| `proxy.host` | `127.0.0.1` | Loopback. Un bind pubblico viene rifiutato senza un token di telemetria. |
| `proxy.port` | `8080` | |
| `proxy.default_provider` | `openai` | Usato quando non viene inviato l'header `x-nullorigin-provider`. |
| `proxy.telemetry_token` | `""` | Protegge `/telemetry` e `/telemetry/reset`. |
| `proxy.max_request_bytes` | `104857600` | 100 MiB; i body più grandi ricevono `413`. |
| `text.paraphraser.backend` | `ollama` | `none` \| `ollama` \| `openai_compatible` \| `llama_cpp` \| `lexical`. `none` lascia intatta la filigrana statistica. `lexical` non richiede un modello ma è un attacco molto più debole. |
| `text.clean_unicode` | `true` | Rimozione di caratteri a larghezza zero e del blocco Tags. |
| `text.fold_homoglyphs` | `true` | Confondibili cirillici/greci convertiti in ASCII. |
| `text.stream_window_tokens` | `40` | Delta bufferizzati prima che uno span di streaming venga riscritto. |
| `media.crop_mode` | `trim` | `trim` sposta le coordinate senza ricampionare; `resample` ripristina le dimensioni esatte ma costa circa SSIM 0.81 / PSNR 31 dB anche con un ritaglio dello 0.5%; `none` disabilita il passaggio geometrico. |
| `audio.low_cut_hz` | `800.0` | La fase al di sotto di questo valore viene preservata per l'intelligibilità. |
### Variabili d'ambiente```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
Il modello configurato non è stato scaricato. Esegui ollama list per vedere cosa hai, poi
ollama pull llama3.2:3b oppure imposta NULLORIGIN_PARAPHRASER_MODEL su un modello che
hai già.
WARNING: ollama backend unavailable (ReadTimeout)
La riscrittura ha superato text.paraphraser.timeout_seconds (predefinito 120 s). I modelli
di ragionamento come qwen3 impiegano regolarmente 150 s+ per paragrafo su CPU. Aumentalo:
export NULLORIGIN_PARAPHRASER_TIMEOUT=900, oppure usa un modello instruct più piccolo.
nullorigin benchmark esce con 1 e pass_no_detection: FAIL
Funziona come previsto. Nessun backend di riscrittura era raggiungibile, quindi è stato
eseguito solo il livello unicode e la filigrana statistica è sopravvissuta. Avvia Ollama,
oppure imposta il backend su lexical per un confronto senza dipendenze.
semantic_check: INCONCLUSIVE
Previsto senza l'extra [metrics]. Vedi Onestà delle metriche.
Error: Refusing to bind 0.0.0.0 without a telemetry token
Intenzionale. Vedi Distribuzione oltre localhost.
Multiple top-level packages discovered in a flat-layout
Sei su un checkout vecchio. pyproject.toml imposta un elenco esplicito di pacchetti; fai
pull dell'ultima versione.
I test async segnalano UsageError per un plugin async mancante
Deliberato — senza uno, pytest segnala i test async def come superati senza eseguirne
l'await. pip install -e ".[dev]".
Docker: curl: (7) Failed to connect subito dopo compose up
L'healthcheck ha un periodo di avvio di 10 s. Attendi, poi controlla
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
### Layout```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 test. La suite copre il parser dei frame SSE contro confini di chunk avversari, il ciclo di vita dello streaming proxy, ogni modalità immagine PIL, SSIM sia rispetto a un valore in forma chiusa sia a un'implementazione di riferimento brute-force, e la rilevabilità delle filigrane nel dataset.
I test asincroni falliscono in modo evidente se non è installato alcun plugin asincrono, piuttosto che essere saltati silenziosamente.
---
## 📖 Citazioni e riuso
Concesso in licenza sotto Apache-2.0, che consente l'uso, la modifica e la ridistribuzione a condizione che vengano mantenuti l'avviso di copyright e l'attribuzione a **Muhammad Rakibul Islam**. Vedere [LICENSE](https://github.com/rakib-nyc/nullorigin/blob/HEAD/LICENSE) e [NOTICE](https://github.com/rakib-nyc/nullorigin/blob/HEAD/NOTICE).
Se questo lavoro supporta una pubblicazione, si prega di citarlo come:```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}
}
Questa repository è pubblicata come artefatto di ricerca completo e non accetta pull request. Sei libero di farne un fork secondo i termini della licenza. Domande e risultati sono benvenuti via email a [email protected].
Versione 1.0.0. La suite è completa rispetto alle sue specifiche e completamente testata, con questi limiti noti:
[nli], gli
scambi di ruolo che preservano ogni entità sono indistinguibili, e il rapporto lo dichiara.Vedi CHANGELOG.md per la cronologia delle release.
Questa repository è un artefatto di livello di ricerca rilasciato per ricerca statistica, valutazione della privacy, benchmarking della robustezza delle watermark e test di resilienza crittografica.```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.
**NESSUNA GARANZIA.** IL SOFTWARE VIENE FORNITO "COSÌ COM'È", SENZA ALCUN TIPO DI GARANZIA,
ESPRESSA O IMPLICITA.
| Livello | Cosa fa realmente |
|---|
| Caratteri invisibili | Pienamente efficace. I payload con larghezza zero, controllo bidi, selettori di variante e blocco Unicode Tags vengono rimossi completamente, con un conteggio riportato. I confondibili omoglifi tra scritture diverse (cirillico/greco resi come ASCII) vengono normalizzati. |
| Metadati del documento (.docx) | Pienamente efficace. Autore, ultimo editor, numero di revisioni, timestamp, modello e versione dell'applicazione vengono cancellati da docProps, con la formattazione preservata byte per byte. |
| C2PA / EXIF / XMP | Pienamente efficace. L'immagine viene ricostruita da campioni di pixel grezzi in un contenitore nuovo, quindi i manifest JUMBF firmati e tutti i metadati spariscono. Verificato tramite test su fixture etichettati. |
| Watermark statistico KGW | Dipende interamente dal backend di riscrittura. Senza un modello locale configurato, il watermark statistico sopravvive — lo strumento lo dice esplicitamente piuttosto che suggerire il contrario. |
| SynthID-Text / SynthID-Image / Tree-Ring | Non verificabile qui. Questi usano chiavi private e decoder proprietari. NullOrigin applica le perturbazioni descritte dalla letteratura, ma non viene fatta alcuna affermazione che sconfiggano i rilevatori reali, perché non esiste un rilevatore pubblico con cui misurarli. |
| AudioSeal / SynthID-Audio | Non verificabile qui, per lo stesso motivo. |
| Opzionale | Docker 20.10+ con Compose v2 |
| caso | lexical_f1 | coseno di embedding | NLI bidirezionale |
|---|
| negazione omessa | 0.70 | 0.77 ✓ | 0.000 ✓ |
| numero 5 → 50 | 0.82 | 0.81 ✓ | 0.000 ✓ |
| scambio entità/ruolo | 0.81 | 0.985 ✗ | 0.000 ✓ |
| quantificatore all → some | 0.88 | 0.91 ✓ | 0.000 ✓ |
| attenuazione rimossa | 0.27 | 0.953 ✗ | 0.011 ✓ |
| "must not" → "must" | 0.83 | 0.947 ✗ | 0.000 ✓ |
| riscrittura fedele | 0.33 | 0.931 ✓ | 0.998 ✓ |