
Boîte à outils de robustesse des filigranes IA à usage exclusivement de recherche : un proxy inverse local supprime C2PA/EXIF/XMP, Unicode, la stéganographie image/audio et les métadonnées OOXML/PDF, et détecte Trojan Source.
Middleware universel de provenance IA et de neutralisation de filigranes Artéfact de recherche — uniquement pour l'évaluation de la robustesse des filigranes.
NullOrigin est un artéfact de recherche. Il est publié pour soutenir l'étude académique et indépendante de la robustesse des filigranes, et pour aucune autre fin.
Les filigranes sont des assertions de sécurité, et ces assertions ne prennent de sens que lorsque quelqu'un a tenté de les briser. La littérature que ce projet implémente — Kirchenbauer et al. sur KGW, Krishna et al. sur les attaques par paraphrase, Boucher & Anderson sur Trojan Source — existe parce que les chercheurs ont publié des attaques fonctionnelles afin que les défenseurs puissent mesurer une robustesse réelle plutôt que de la supposer. C'est la tradition à laquelle appartient ce dépôt.
Usages prévus
Non prévu, et non pris en charge
Rien ici n'est un contrôle technique sur la manière dont le code s'exécute. C'est une déclaration des conditions auxquelles il est fourni, et de ce que son auteur soutiendra ou non. Le logiciel est fourni « EN L'ÉTAT », sans garantie d'aucune sorte — voir LICENSE.
Lisez Portée et limites honnêtes avant de tirer une conclusion à partir d'un nombre affiché par cet outil. Plusieurs des mécanismes qu'il cible ne peuvent pas être vérifiés auprès d'un détecteur public, et le README le dit plutôt que de laisser entendre le contraire.
Lisez ceci avant de tirer des conclusions à partir d'un nombre affiché par cet outil.
KGWStatisticalDetector est une implémentation mathématiquement fidèle et cohérente du
mécanisme de liste verte/rouge de Kirchenbauer et al. sur les jetons d'espacement. Ce n'est pas un
décodeur pour le filigrane de production d'un fournisseur — ceux-ci reposent sur un secret privé et sur
le vocabulaire BPE propre au modèle.
Son objectif est de rendre l'analyse comparative réelle : KGWWatermarkEmbedder insère un véritable
filigrane, le pipeline l'attaque, et le détecteur correspondant mesure la réduction effective. C'est une
mesure réelle de l'attaque contre ce mécanisme. Elle ne se transpose pas au filigrane d'un fournisseur.
Le vocabulaire $V$ est partitionné à chaque étape $t$ par un hachage initialisé sur le contexte précédent :
$$s_t = \text{Hash}(w_{t-k}, \dots, w_{t-1})$$
en une liste verte $G_t$ de taille $\gamma|V|$ et une liste rouge $R_t$. Un biais $\delta > 0$ est ajouté aux logits verts :
$$\tilde{l}{t,v} = \begin{cases} l{t,v} + \delta, & v \in G_t \\ l_{t,v}, & v \in R_t \end{cases}$$
La détection compte les occurrences vertes. Sous $H_0$, elles suivent une $\text{Binomiale}(T, \gamma)$, donc :
$$z = \frac{|S_G| - \gamma T}{\sqrt{T\gamma(1-\gamma)}}$$
avec $z > 4.0$ ($p < 3\times10^{-5}$) signalé comme synthétique.
Pourquoi la paraphrase l'attaque : le filigrane réside entièrement dans les transitions locales de n-grammes. Réécrire la forme de surface avec un modèle non filigrané réinitialise chaque position. C'est l'attaque de robustesse standard dans la littérature sur les filigranes.
Pourquoi la longueur compte : $z$ croît comme $\sqrt{T}$. Un passage de 100 jetons avec une fraction verte de 0.70 n'atteint que $z \approx 3.9$ — sous le seuil. La détection nécessite quelques centaines de jetons, tout comme des maquettes d'analyse comparative pertinentes.
APP11, les blocs PNG tEXt/iTXt,
ou les boîtes c2pa WebP/AVIF. Comme la signature couvre les données de pixels, un ré-encodage à partir
d'un tampon d'échantillons brut le supprime sans même analyser JUMBF.Modulation de phase sous le seuil et ajouts spectraux de faible amplitude. Attaqué par randomisation de phase au-dessus de la fondamentale de la parole, décalage d'encoches coupe-bande dans les bandes non critiques, et re-quantification psychoacoustique.
| Python | 3.10, 3.11 ou 3.12 |
| OS | Linux, macOS (Intel et Apple Silicon), Windows via WSL2 |
| Optionnel | Ollama ou tout serveur compatible 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 .
### Extras facultatifs```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]"
Sans
[nli], la porte de fidélité s'exécute uniquement sur les invariants — un contrôle réel quand même, mais aveugle aux échanges de rôles. Voir Fidélité sémantique.
nullorigin --version nullorigin --help pytest -q # requires the [dev] extra
---
## 🚀 Démarrage rapide
### 1. Configurer un modèle de réécriture local
Le dé-watermarking de texte nécessite un modèle local non filigrané. Sans cela, NullOrigin supprime
les caractères invisibles mais **laisse le filigrane statistique intact** — et le précise.```bash
ollama serve # in a separate terminal
ollama pull llama3.2:3b # or any instruct model you prefer
Vous utilisez un autre modèle ? Pointez NullOrigin dessus :```bash export NULLORIGIN_PARAPHRASER_MODEL=qwen3:4b export NULLORIGIN_PARAPHRASER_TIMEOUT=900 # reasoning models are slow
### 2. Démarrer le 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. Pointez votre client dessus```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"}]}'
Le streaming (SSE) et Gemini (/v1beta/models/...) sont traités de la même manière. L'en-tête
x-nullorigin-provider sélectionne le fournisseur en amont et est supprimé avant le transfert ;
vos en-têtes d'authentification transitent sans modification.
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
La pile Compose exécute NullOrigin plus un sidecar Ollama sur un réseau ponté privé.
Le conteneur proxy se lie à `0.0.0.0` — correct à l'intérieur d'un conteneur — et seul le port 8080 est
publié vers votre hôte.
Image autonome:```bash
docker build -t nullorigin:1.0.0 .
docker run -d -p 8080:8080 \
-e NULLORIGIN_PARAPHRASER_BACKEND=none \
nullorigin:1.0.0
Commandes utiles :```bash docker compose logs -f nullorigin docker compose down # stop docker compose down -v # stop and delete the Ollama model volume
---
## 🔒 Déploiement au-delà de localhost
**NullOrigin est configuré par défaut sur `127.0.0.1` et refuse de se lier à une interface publique sans
jeton de télémétrie.** Il relaie vos identifiants API en amont, c'est donc délibéré :```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
Pour l'exposer correctement :```bash export NULLORIGIN_TELEMETRY_TOKEN=$(openssl rand -hex 32) nullorigin run --host 0.0.0.0 --port 8080
puis placez-le derrière nginx, Caddy ou Traefik assurant la **terminaison TLS**, la **limitation
de débit** et une **couche d'authentification**.
### Modèle de menace
NullOrigin est un **proxy inverse local qui relaie vos identifiants d'API en amont**. Cette unique caractéristique détermine sa posture de sécurité.
| Contrôle | Par défaut | Raison |
| --- | --- | --- |
| Adresse d'écoute | `127.0.0.1` | Boucle locale uniquement ; une liaison publique est refusée sauf si un jeton de télémétrie est défini ou si `--allow-public-bind` est passé. |
| `/telemetry`, `/telemetry/reset` | Ouvert en boucle locale | Protégé par `X-NullOrigin-Token`, comparé en temps constant, dès lors que `proxy.telemetry_token` est défini. |
| `/health` | Toujours ouvert | Les sondes de conteneurs en ont besoin ; expose la version et les moteurs activés, aucun secret. |
| Taille du corps de requête | 100 MiB | Le proxy met en mémoire tampon les corps pour les transférer ; toute entrée plus grande est rejetée avec `413`. |
| Tampon SSE | 1 MiB | Un serveur amont qui ne termine jamais une trame est vidé, pas mis en mémoire tampon indéfiniment. |
| Utilisateur du conteneur | non-root | Le proxy n'a besoin d'aucun privilège élevé. |
Limitations connues, par conception et non des défauts :
* **Pas de TLS.** Il transmet `Authorization` et `x-api-key` tels quels sur HTTP en clair. Placez-le derrière un proxy inverse qui termine HTTPS sur tout réseau non fiable.
* **Aucune authentification sur le chemin du proxy.** Toute personne qui peut atteindre le port peut l'utiliser comme proxy avec ses propres identifiants — NullOrigin ne stocke ni n'injecte de clés.
* **Aucune limitation de débit.** Appliquez-la au niveau du proxy inverse.
* **Les identifiants ne sont jamais persistés.** Aucune clé API n'est écrite sur le disque ou dans les journaux ; la télémétrie compte uniquement les requêtes et les événements de nettoyage.
* La vérification TLS en amont reste activée et les redirections ne sont pas suivies.
Pour signaler un problème de sécurité, envoyez un e-mail à **[email protected]** avec `[NullOrigin Security]` dans l'objet.
Télémétrie avec un jeton défini :```bash
curl -H "X-NullOrigin-Token: $NULLORIGIN_TELEMETRY_TOKEN" http://localhost:8080/telemetry
/health n'est jamais protégé, donc les sondes de conteneur continuent de fonctionner.
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...]
### Formats pris en charge
| Type | Extensions | Notes |
| --- | --- | --- |
| **Images** | `.png` `.jpg` `.jpeg` `.jfif` `.webp` `.tif` `.tiff` `.bmp` `.gif` `.ico` `.avif` `.jp2` | Tous les modes PIL (RGB, RGBA, L, LA, P, 1, I;16, CMYK, YCbCr). Les GIF/WebP animés et les TIFF multipages conservent chaque image et leur timing. Les images de moins de 64 px conservent leurs dimensions exactes. |
| **Audio** | `.wav` `.wave` | Entiers sur 8/16/32 bits, flottant 32 bits ; du mono au multicanal ; n'importe quelle fréquence d'échantillonnage. Les fichiers de longueur nulle survivent à l'aller-retour. |
| **Documents** | `.docx` `.docm` `.dotx` `.pptx` `.pptm` `.xlsx` `.xlsm` | Les trois dialectes OOXML. Les séquences de texte sont assainies dans le corps, les en-têtes, les pieds de page, les notes de bas de page, les notes et les chaînes partagées ; les métadonnées `docProps` sont supprimées ; toutes les autres parties sont copiées octet par octet. |
| **PDF** | `.pdf` | Dictionnaire `/Info`, paquet XMP, pièces jointes et JavaScript supprimés ; pages, texte et géométrie préservés. Les fichiers protégés par mot de passe sont refusés. Voir la mise en garde ci-dessous. |
| **Code source** | `.py` `.js` `.ts` `.go` `.rs` `.java` `.c` `.cpp` `.rb` `.php` `.sh` `.sql` + 50 autres | Analyse Trojan Source et homoglyphes. **Pas de NFKC, pas de paraphrase** — voir ci-dessous. |
| **Texte** | tout le reste décodable | UTF-8, UTF-8 BOM, UTF-16, UTF-32, CP1252, Latin-1 — détecté automatiquement et **réécrit dans le même encodage**. |
Tout le reste est **refusé avec une indication précise** plutôt que lu en UTF-8 et corrompu — `.mp3` renvoie vers `ffmpeg -i in.mp3 out.wav`, les anciens `.doc`/`.ppt`/`.xls` vers un réenregistrement en OOXML. Un fichier refusé ne produit jamais de sortie.
Vérifié sur un corpus de 69 fichiers couvrant tous les formats ci-dessus : **59 traités correctement, 10 refusés proprement, zéro plantage, zéro sortie corrompue.**
### Le code est tenu à l'écart de la réécriture
Les réponses de l'assistant mélangent prose et code dans une seule chaîne. Confier l'ensemble à un modèle de paraphrase réécrit le code en même temps que la prose — et le score z chute dans les deux cas, donc rien en aval ne le remarque.
Les réponses sont donc segmentées avant toute réécriture :
| Segment | Traitement |
| --- | --- |
| Prose | Nettoyée Unicode, puis réécrite |
| Blocs délimités (``` et ~~~) | Caractères invisibles et bidi supprimés. **Pas de NFKC, jamais réécrits.** |
| Segments `` `code` `` en ligne | Idem |
Cela vaut aussi sur le chemin de streaming, où un délimiteur s'ouvre dans un delta et se ferme plusieurs deltas plus tard. Un delta à cheval sur la limite est découpé ligne par ligne, si bien que le ``` de fermeture et la prose qui suit sont traités différemment. Un délimiteur non fermé échoue en toute sécurité : le reste est protégé plutôt que réécrit.
Désactivez avec `text.protect_code_blocks: false` si vous voulez l'ancien comportement.
### Fichiers de code source : une analyse de sécurité, pas une suppression de filigrane
**Il n'y a pas de filigrane dans le code source généré par IA.** Aucun fournisseur ne filigrane la sortie de code, et aucun détecteur public n'existe. Quiconque prétend en retirer un vous vend quelque chose.
Ce que le code source *a* en revanche, c'est une surface d'attaque réelle et documentée :
* **Trojan Source** ([CVE-2021-42574](https://nvd.nist.gov/vuln/detail/CVE-2021-42574), Boucher & Anderson 2021) — les caractères de contrôle bidirectionnels réordonnent la façon dont le code est *affiché* sans changer la façon dont il *compile*. Un relecteur approuve un programme ; le compilateur en construit un autre.
* **Identifiants homoglyphes** ([CVE-2021-42694](https://nvd.nist.gov/vuln/detail/CVE-2021-42694)) — le cyrillique `а` pour le latin `a` crée deux noms qui s'affichent de manière identique.```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":
Après purge, on lit if access_level != "user // Check if admin": — le
« commentaire » se trouvait en réalité à l'intérieur de la chaîne.
Trois choses que le chemin de code ne fait volontairement pas, parce que le chemin de texte générique faisait les trois et chacune est un bug sur la source :
"Hello" devient
"Hello", "office" devient "office". Cela change ce qu'un programme compare, hache,
et transmet.а cyrillique fusionne deux identifiants
que le compilateur traite actuellement comme distincts — changeant silencieusement le comportement. La gravité n'est
MEDIUM que pour les tokens à écritures mixtes (totаl), la véritable signature d'attaque ; un mot
entièrement écrit dans une autre écriture est un texte étranger ordinaire et reçoit INFO. Activez
--fold-homoglyphs-in-code une fois que vous les avez examinés.inspect --json émet pour chaque résultat la gravité, la ligne, la colonne et le point de code, ce qui lui permet de s'intégrer
dans un CI comme étape de validation pre-commit ou PR.
Supprimés, de manière vérifiable : le dictionnaire /Info (Author, Title, Subject, Keywords,
Creator, Producer, CreationDate, ModDate), le paquet XMP dans /Root/Metadata, les pièces
jointes intégrées et le JavaScript au niveau du document. Les pages, le texte et la géométrie des pages sont
préservés exactement ; l'opération est idempotente et stable au niveau des octets.
Détectés mais PAS supprimés : les caractères invisibles dans les flux de contenu des pages. Le PDF
dessine le texte glyphe par glyphe via un encodage spécifique à la police — un espace de largeur nulle dans une
police à clés CID est un index de glyphe sur deux octets, pas un U+200B littéral — donc une réécriture générique
corromprait la mise en page au lieu de la nettoyer. inspect signale le nombre ; purge
affiche un avertissement plutôt que de rester silencieux, car le silence pourrait être interprété comme « il n'y en avait
aucun ». Pour les supprimer, extrayez le texte, exécutez nullorigin purge dessus, puis régénérez
le PDF.
--strip-annotations est disponible mais désactivé par défaut : les annotations incluent les liens et
les champs de formulaire, pas seulement les commentaires, donc les supprimer change le comportement du document.
Les tirets cadratins, les guillemets courbes et les points de suspension sont une sortie ordinaire de traitement de texte. NullOrigin
les préserve par défaut et les signale séparément des résultats authentiques, car
les aplatir dégrade un document sans rien assainir. Utilisez
--flatten-typography si vous voulez spécifiquement une sortie ASCII.
Les confusables entre écritures sont différents — un о cyrillique au milieu d'un mot dans un texte anglais n'a aucun
usage légitime — et ceux-ci sont repliés par défaut.
--verify rapporte des mesures avant/après plutôt que d'affirmer le succès :```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
Si le backend est inaccessible, cela est signalé comme un avertissement sur stderr — un repli silencieux
serait identique à un assainissement réussi.
---
## 📊 Benchmarking```bash
nullorigin build-datasets
nullorigin benchmark
Chaque valeur est mesurée sur place : le texte est filigrané avec KGWWatermarkEmbedder, exécuté via le pipeline réel, puis ré-évalué avec le détecteur correspondant. Le runner se termine avec un code non nul lorsque les seuils ne sont pas atteints et explique pourquoi.
Seuils (issus de la directive du projet) :
| Métrique | Cible |
|---|---|
| z-score post-assainissement | $\lvert z\rvert \le 1.5$ |
| Similarité sémantique | $\ge 0.92$ |
| SSIM de l'image | $\ge 0.95$ |
| PSNR de l'image | $\ge 36$ dB |
Section de texte intégral sur datasets/text/watermarked_kgw.json, réécrit via Ollama (qwen3:4b) sur un MacBook série M (~150 s par passage) :```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)
Chaque échantillon filigrané est passé de détecté à non détecté. Notons `kgw_003` à z = 1.271 — sous le seuil mais le plus proche de celui-ci, ce qui est la forme honnête de cette attaque : elle est statistique, pas une garantie.
Média, mesuré sur les fixtures d'images :```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
Les deux seuils d'image sont satisfaits (SSIM ≥ 0.95, PSNR ≥ 36 dB). Les valeurs varieront selon le modèle, le matériel et le passage.
Une réécriture qui modifie un fait obtient exactement le même score qu'une réécriture fidèle sur la métrique du filigrane. La vérification évidente pour cela ne fonctionne pas, et la moins évidente non plus. Mesuré sur six cas de dérive plus un témoin fidèle :
Le chevauchement lexical est inversé. Chaque modification détruisant le sens a obtenu un score plus élevé que la réécriture fidèle, car une bonne paraphrase partage peu de n-grammes avec sa source alors qu'une version corrompue en partage presque tous.
Le cosinus des embeddings ne corrige pas cela. Trois des six cas corrompus franchissent un seuil de 0.92. "Alice paid Bob" et "Bob paid Alice" sont le même sac de mots et obtiennent 0.985 ; "must not disable" → "must disable" obtient 0.947. Les embeddings de phrases encodent la parenté thématique, pas la vérité.
La fidélité est donc vérifiée à deux niveaux, et aucun n'est le cosinus :
negation count changed: 1 → 0). Les modaux et quantificateurs sont comparés par classe de sens, donc may → might passe et may → must échoue. Aveugle aux inversions de rôles où chaque entité survit.nullorigin[nli] ; sans lui, la limitation est signalée, pas cachée.Mesurer la dérive après coup n'aide pas si le texte endommagé a déjà été renvoyé. Un contrôle qui échoue réessaie à une température plus basse — la dérive est pilotée par la température — et une fois le budget de réessais épuisé, renvoie l'original, avec ok=False et la raison.```yaml
text:
fidelity:
enabled: true
max_retries: 2
temperature_step: 0.25
use_nli: true
nli_threshold: 0.5
Cela signifie aussi que l'attaque et le risque partagent un même curseur : augmenter la température abaisse le
z-score *et* augmente le taux de dérive. Le benchmark les rapporte ensemble plutôt que comme
des contrôles indépendants.
### Autres métriques
* **Perplexité** — vraie PPL GPT-2 avec `torch` + `transformers`, sinon
`unigram_entropy_proxy`, signalée comme approximative et **non** comparable aux PPL publiées.
* **Similarité cosinus** est toujours rapportée comme `mean_cosine_or_lexical`, à titre de référence uniquement.
Elle ne constitue plus un critère de réussite/échec, pour les raisons indiquées dans le tableau ci-dessus.
## ⚙️ Configuration
Ordre de résolution, de la priorité la plus basse à la plus haute :
1. Valeurs par défaut intégrées
2. `nullorigin.yaml` (recherché dans `./`, `../`, `/app/`, ou `$NULLORIGIN_CONFIG`)
3. Variables d'environnement `NULLORIGIN_*`
4. Drapeaux CLI explicites
### Paramètres clés
| Setting | Default | Notes |
| --- | --- | --- |
| `proxy.host` | `127.0.0.1` | Boucle locale. Une liaison publique est refusée sans jeton de télémétrie. |
| `proxy.port` | `8080` | |
| `proxy.default_provider` | `openai` | Utilisé lorsqu'aucun en-tête `x-nullorigin-provider` n'est envoyé. |
| `proxy.telemetry_token` | `""` | Protège `/telemetry` et `/telemetry/reset`. |
| `proxy.max_request_bytes` | `104857600` | 100 MiB ; les corps plus volumineux reçoivent `413`. |
| `text.paraphraser.backend` | `ollama` | `none` \| `ollama` \| `openai_compatible` \| `llama_cpp` \| `lexical`. `none` laisse le filigrane statistique intact. `lexical` ne nécessite aucun modèle mais constitue une attaque bien plus faible. |
| `text.clean_unicode` | `true` | Suppression des caractères de largeur nulle et du bloc Tags. |
| `text.fold_homoglyphs` | `true` | Confusables cyrilliques/grecs convertis en ASCII. |
| `text.stream_window_tokens` | `40` | Deltas mis en mémoire tampon avant que le segment de streaming soit réécrit. |
| `media.crop_mode` | `trim` | `trim` déplace les coordonnées sans rééchantillonnage ; `resample` restaure les dimensions exactes mais coûte environ SSIM 0,81 / PSNR 31 dB même pour un recadrage de 0,5 % ; `none` désactive le passage géométrique. |
| `audio.low_cut_hz` | `800.0` | La phase en dessous de cette valeur est préservée pour l'intelligibilité. |
### Variables d'environnement```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
Le modèle configuré n'a pas été récupéré. Exécutez ollama list pour voir ce que vous avez, puis soit
ollama pull llama3.2:3b, soit définissez NULLORIGIN_PARAPHRASER_MODEL sur un modèle que vous avez déjà.
WARNING: ollama backend unavailable (ReadTimeout)
La réécriture a dépassé text.paraphraser.timeout_seconds (120 s par défaut). Les modèles de raisonnement
tels que qwen3 prennent régulièrement plus de 150 s par paragraphe sur CPU. Augmentez-le :
export NULLORIGIN_PARAPHRASER_TIMEOUT=900, ou utilisez un modèle instruct plus petit.
nullorigin benchmark exits 1 with pass_no_detection: FAIL
Comportement attendu. Aucun backend de réécriture n'était joignable, donc seule la couche Unicode a été exécutée et
le filigrane statistique a survécu. Démarrez Ollama, ou définissez le backend sur lexical pour une
comparaison sans dépendance.
semantic_check: INCONCLUSIVE
Attendu sans l'extra [metrics]. Voir Honnêteté des métriques.
Error: Refusing to bind 0.0.0.0 without a telemetry token
Volontaire. Voir Déploiement au-delà de localhost.
Multiple top-level packages discovered in a flat-layout
Vous êtes sur un ancien checkout. pyproject.toml définit une liste de paquets explicite ; récupérez la dernière version.
Async tests report UsageError about a missing async plugin
Délibéré — sans lui, pytest signale les tests async def comme réussis sans les attendre.
Exécutez pip install -e ".[dev]".
Docker: curl: (7) Failed to connect right after compose up
Le healthcheck a une période de démarrage de 10 s. Attendez, puis vérifiez 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
### Mise en page```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 tests. La suite de tests couvre l'analyseur de trames SSE face à des frontières de chunks adverses, le cycle de vie
du streaming proxy, chaque mode d'image PIL, le SSIM par rapport à une valeur de forme fermée et à une
implémentation de référence par force brute, ainsi que la détectabilité du filigrane dans les ensembles de données.
Les tests asynchrones échouent bruyamment si aucun plugin asynchrone n'est installé, plutôt que d'être silencieusement ignorés.
---
## 📖 Citation et réutilisation
Sous licence Apache-2.0, qui autorise l'utilisation, la modification et la redistribution à condition
que l'avis de droit d'auteur et l'attribution à **Muhammad Rakibul Islam** soient conservés. Voir
[LICENSE](https://github.com/rakib-nyc/nullorigin/blob/HEAD/LICENSE) et [NOTICE](https://github.com/rakib-nyc/nullorigin/blob/HEAD/NOTICE).
Si ce travail est utilisé à l'appui d'une publication, veuillez le citer comme suit :```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}
}
Ce dépôt est publié comme un artefact de recherche abouti et n'accepte pas les pull requests. Vous êtes libre de le forker selon les termes de la licence. Les questions et les découvertes sont les bienvenues par courriel à [email protected].
Version 1.0.0. La suite est complète par rapport à sa spécification et entièrement testée, avec les limites connues suivantes :
[nli], les échanges de rôles qui préservent chaque entité sont indétectables, et le rapport le dit.Voir CHANGELOG.md pour l'historique des versions.
Ce dépôt est un artefact de qualité recherche publié pour la recherche statistique, l'évaluation de la confidentialité, l'analyse comparative de la robustesse des filigranes et les tests de résilience cryptographique.```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.
**AUCUNE GARANTIE.** LE LOGICIEL EST FOURNI "TEL QUEL", SANS GARANTIE D'AUCUNE SORTE,
EXPRESSE OU IMPLICITE.
| Couche | Ce qu'elle fait réellement |
|---|
| Caractères invisibles | Totalement efficace. Les charges utiles à largeur nulle, de contrôle bidi, de sélecteur de variante et du bloc Unicode Tags sont entièrement supprimées, avec un décompte indiqué. Les confusables homoglyphes inter-scriptes (cyrillique/grec rendus en ASCII) sont repliés. |
| Métadonnées de document (.docx) | Totalement efficace. L'auteur, le dernier éditeur, le nombre de révisions, les horodatages, le modèle et la version de l'application sont effacés de docProps, avec mise en forme préservée octet par octet. |
| C2PA / EXIF / XMP | Totalement efficace. L'image est reconstruite à partir des échantillons bruts de pixels dans un nouveau conteneur, de sorte que les manifestes JUMBF signés et toutes les métadonnées disparaissent. Vérifié par des tests sur des maquettes étiquetées. |
| Filigrane statistique KGW | Dépend entièrement du backend de réécriture. Sans modèle local configuré, le filigrane statistique survit — l'outil le dit plutôt que de laisser entendre le contraire. |
| SynthID-Text / SynthID-Image / Tree-Ring | Non vérifiable ici. Ces mécanismes utilisent des clés privées et des décodeurs propriétaires. NullOrigin applique les perturbations décrites dans la littérature, mais aucune affirmation n'est faite selon laquelle elles neutralisent les véritables détecteurs, car il n'existe aucun détecteur public pour effectuer une mesure. |
| AudioSeal / SynthID-Audio | Non vérifiable ici, pour la même raison. |
| Optionnel | Docker 20.10+ avec Compose v2 |
| cas | lexical_f1 | embedding cosine | bidirectional NLI |
|---|
| négation supprimée | 0.70 | 0.77 ✓ | 0.000 ✓ |
| nombre 5 → 50 | 0.82 | 0.81 ✓ | 0.000 ✓ |
| inversion entité/rôle | 0.81 | 0.985 ✗ | 0.000 ✓ |
| quantificateur all → some | 0.88 | 0.91 ✓ | 0.000 ✓ |
| atténuateur retiré | 0.27 | 0.953 ✗ | 0.011 ✓ |
| "must not" → "must" | 0.83 | 0.947 ✗ | 0.000 ✓ |
| réécriture fidèle | 0.33 | 0.931 ✓ | 0.998 ✓ |