
Kit de ferramentas de robustez de marcas d'água de IA somente para pesquisa: proxy reverso local remove C2PA/EXIF/XMP, Unicode, esteganografia de imagem/áudio, metadados OOXML/PDF e verifica Trojan Source.
Middleware Universal de Proveniência e Sanitização de Marcas d'Água de IA Artefato de pesquisa — somente para avaliação de robustez de marcas d'água.
NullOrigin é um artefato de pesquisa. Ele é publicado para apoiar o estudo acadêmico e independente da robustez de marcas d'água, e para nenhum outro propósito.
Esquemas de marca d'água são alegações de segurança, e alegações de segurança só são significativas quando alguém tentou quebrá-las. A literatura que este projeto implementa — Kirchenbauer et al. sobre KGW, Krishna et al. sobre ataques de paráfrase, Boucher & Anderson sobre Trojan Source — existe porque pesquisadores publicaram ataques funcionais para que defensores pudessem medir robustez real em vez de assumi-la. Essa é a tradição à qual este repositório pertence.
Usos pretendidos
Não pretendido e não suportado
Nada aqui é um controle técnico sobre como o código executa. É uma declaração dos termos sob os quais ele é oferecido, e do que seu autor apoiará ou não. O software é fornecido "AS IS", sem garantia de qualquer tipo — consulte LICENSE.
Leia Escopo e limitações honestas antes de tirar qualquer conclusão de um número que esta ferramenta imprimir. Vários dos esquemas que ela alvo não podem ser verificados contra um detector público, e o README afirma isso em vez de dar a entender o contrário.
Leia isto antes de tirar conclusões de qualquer número que esta ferramenta imprimir.
KGWStatisticalDetector é uma implementação matematicamente fiel, autoconsistente, do
esquema de lista verde/vermelha de Kirchenbauer et al. sobre tokens de espaço em branco. Ele
não é um decodificador para a marca d'água de produção de nenhum fornecedor — essas
dependem de um segredo privado e do vocabulário BPE do próprio modelo.
Seu propósito é tornar o benchmark real: KGWWatermarkEmbedder insere uma marca d'água
genuína, o pipeline a ataca, e o detector correspondente mede a redução real. Essa é uma
medição verdadeira do ataque contra este esquema. Ela não se transfere para a marca d'água de
um fornecedor.
O vocabulário $V$ é particionado a cada passo $t$ por um hash semeado no contexto anterior:
$$s_t = \text{Hash}(w_{t-k}, \dots, w_{t-1})$$
em uma lista verde $G_t$ de tamanho $\gamma|V|$ e uma lista vermelha $R_t$. Um viés $\delta > 0$ é adicionado aos logits verdes:
$$\tilde{l}{t,v} = \begin{cases} l{t,v} + \delta, & v \in G_t \\ l_{t,v}, & v \in R_t \end{cases}$$
A detecção conta acertos verdes. Sob $H_0$ eles são $\text{Binomial}(T, \gamma)$, portanto:
$$z = \frac{|S_G| - \gamma T}{\sqrt{T\gamma(1-\gamma)}}$$
com $z > 4.0$ ($p < 3\times10^{-5}$) marcado como sintético.
Por que a paráfrase ataca isso: a marca d'água vive inteiramente em transições locais de n-gramas. Reescrever a forma de superfície com um modelo sem marca d'água re-semeia cada posição. Este é o ataque de robustez padrão na literatura de marcas d'água.
Por que o comprimento importa: $z$ cresce como $\sqrt{T}$. Uma passagem de 100 tokens com uma fração verde de 0,70 atinge apenas $z \approx 3,9$ — abaixo do limite. A detecção precisa de algumas centenas de tokens, e o mesmo vale para fixtures de benchmark significativos.
APP11, chunks PNG tEXt/iTXt,
ou caixas c2pa WebP/AVIF. Como a assinatura cobre os dados de pixel, a re-codificação
a partir de um buffer de amostra puro remove-a sem analisar JUMBF.Modulação de fase abaixo do limiar e adições espectrais de baixa amplitude. Atacado por randomização de fase acima do fundamental da fala, deslocamento de entalhe de rejeição de banda em bandas não críticas e re-quantização psicoacústica.
| Python | 3.10, 3.11 ou 3.12 |
| SO | Linux, macOS (Intel e Apple Silicon), Windows via WSL2 |
| Opcional | Ollama ou qualquer servidor compatível com 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 opcionais```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]"
Sem
[nli], a gate de fidelidade roda apenas sobre invariantes — ainda é uma verificação real, mas cega a trocas de papéis. Veja Fidelidade semântica.
nullorigin --version nullorigin --help pytest -q # requires the [dev] extra
---
## 🚀 Início rápido
### 1. Configure um modelo de reescrita local
A remoção de marca d'água em texto precisa de um modelo local sem marca d'água. Sem um, o NullOrigin remove
caracteres invisíveis, mas **deixa a marca d'água estatística intacta** — e avisa isso.```bash
ollama serve # in a separate terminal
ollama pull llama3.2:3b # or any instruct model you prefer
Usando um modelo diferente? Aponte o NullOrigin para ele:```bash export NULLORIGIN_PARAPHRASER_MODEL=qwen3:4b export NULLORIGIN_PARAPHRASER_TIMEOUT=900 # reasoning models are slow
### 2. Inicie o proxy```bash
nullorigin run
[No content provided 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. Aponte seu cliente para ele```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)
(translated into Portuguese):The following content combines the system/developer instructions and the input received. The Assistant response must comply with the instruction to translate the chunk, so the output below is the Assistant's final response, containing only the translated Markdown text.
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/...) são tratados da mesma forma. O
cabeçalho x-nullorigin-provider seleciona o upstream e é removido antes do encaminhamento;
seus cabeçalhos de autenticação passam intocados.
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
A stack Compose executa o NullOrigin junto com um sidecar Ollama em uma rede bridge privada.
O contêiner proxy faz bind de `0.0.0.0` — correto dentro de um contêiner — e apenas a porta 8080 é
publicada para o seu host.
Imagem standalone:```bash
docker build -t nullorigin:1.0.0 .
docker run -d -p 8080:8080 \
-e NULLORIGIN_PARAPHRASER_BACKEND=none \
nullorigin:1.0.0
Comandos úteis:```bash docker compose logs -f nullorigin docker compose down # stop docker compose down -v # stop and delete the Ollama model volume
---
## 🔒 Implantação além do localhost
**O NullOrigin usa `127.0.0.1` como padrão e se recusa a vincular uma interface pública sem um
token de telemetria.** Ele retransmite as credenciais da sua API upstream, então isso é deliberado:```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
Para expô-lo adequadamente:```bash export NULLORIGIN_TELEMETRY_TOKEN=$(openssl rand -hex 32) nullorigin run --host 0.0.0.0 --port 8080
então coloque-o atrás do nginx, Caddy ou Traefik fornecendo **terminação TLS**, **limitação
de taxa** e uma **camada de autenticação**.
### Modelo de ameaça
O NullOrigin é um **proxy reverso local que retransmite suas credenciais de API do upstream**. Esse
único fato determina sua postura de segurança.
| Controle | Padrão | Motivo |
| --- | --- | --- |
| Endereço de bind | `127.0.0.1` | Somente loopback; um bind público é recusado a menos que um token de telemetria seja definido ou `--allow-public-bind` seja passado. |
| `/telemetry`, `/telemetry/reset` | Aberto no loopback | Controlado por `X-NullOrigin-Token`, comparado em tempo constante, sempre que `proxy.telemetry_token` estiver definido. |
| `/health` | Sempre aberto | Sondas de contêiner precisam dele; expõe versão e mecanismos habilitados, sem segredos. |
| Tamanho do corpo da requisição | 100 MiB | O proxy armazena os corpos em buffer para encaminhá-los; entradas maiores são rejeitadas com `413`. |
| Buffer SSE | 1 MiB | Um upstream que nunca encerra um frame é liberado, não armazenado em buffer indefinidamente. |
| Usuário do contêiner | non-root | O proxy não precisa de privilégios elevados. |
Limitações conhecidas, por design e não defeitos:
* **Sem TLS.** Ele encaminha `Authorization` e `x-api-key` sem alterações por HTTP simples. Coloque-o
atrás de um proxy reverso que faça a terminação HTTPS em qualquer rede não confiável.
* **Sem autenticação no caminho do proxy.** Qualquer pessoa que consiga alcançar a porta pode fazer proxy
através dela, usando suas próprias credenciais — o NullOrigin não armazena nem injeta chaves.
* **Sem limitação de taxa.** Aplique-a no proxy reverso.
* **As credenciais nunca são persistidas.** Nenhuma chave de API é gravada em disco ou em logs; a telemetria
conta apenas requisições e eventos de sanitização.
* A verificação de TLS do upstream permanece ativada, e redirecionamentos não são seguidos.
Para relatar um problema de segurança, envie um e-mail para **[email protected]** com `[NullOrigin Security]` no
assunto.
Telemetria com um token definido:```bash
curl -H "X-NullOrigin-Token: $NULLORIGIN_TELEMETRY_TOKEN" http://localhost:8080/telemetry
/health nunca é protegido, então os probes de contêiner continuam funcionando.
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...]
### Formatos suportados
| Tipo | Extensões | Notas |
| --- | --- | --- |
| **Imagens** | `.png` `.jpg` `.jpeg` `.jfif` `.webp` `.tif` `.tiff` `.bmp` `.gif` `.ico` `.avif` `.jp2` | Todos os modos PIL (RGB, RGBA, L, LA, P, 1, I;16, CMYK, YCbCr). GIF/WebP animados e TIFF multipágina mantêm todos os quadros e sua temporização. Imagens com menos de 64px mantêm as dimensões exatas. |
| **Áudio** | `.wav` `.wave` | Int de 8/16/32 bits, float de 32 bits; de mono a multicanal; qualquer taxa de amostragem. Arquivos de tamanho zero completam o ciclo sem perdas. |
| **Documentos** | `.docx` `.docm` `.dotx` `.pptx` `.pptm` `.xlsx` `.xlsm` | Todos os três dialetos OOXML. Runs de texto sanitizados no corpo, cabeçalhos, rodapés, notas de rodapé, notas e strings compartilhadas; metadados `docProps` removidos; todas as outras partes copiadas byte a byte. |
| **PDF** | `.pdf` | Dicionário `/Info`, pacote XMP, anexos incorporados e JavaScript removidos; páginas, texto e geometria preservados. Arquivos protegidos por senha são recusados. Veja a ressalva abaixo. |
| **Código-fonte** | `.py` `.js` `.ts` `.go` `.rs` `.java` `.c` `.cpp` `.rb` `.php` `.sh` `.sql` + 50 outros | Verificação de Trojan Source e homóglifos. **Sem NFKC, sem parafraseamento** — veja abaixo. |
| **Texto** | qualquer outra coisa decodificável | UTF-8, UTF-8 BOM, UTF-16, UTF-32, CP1252, Latin-1 — detectado automaticamente e **gravado de volta na mesma codificação**. |
Todo o resto é **recusado com orientação específica** em vez de ser lido como UTF-8 e
corrompido — `.mp3` aponta para `ffmpeg -i in.mp3 out.wav`, `.doc`/`.ppt`/`.xls` legados
para re-salvar como OOXML. Um arquivo recusado nunca produz saída.
Verificado contra um corpus de 69 arquivos abrangendo todos os formatos acima: **59 processados corretamente,
10 recusados de forma limpa, zero travamentos, zero saídas corrompidas.**
### O código é mantido fora do reescritor
As respostas do assistente misturam prosa e código em uma única string. Entregar tudo a um
modelo de parafraseamento reescreve o código junto com a prosa — e o z-score cai
de qualquer forma, então nada a jusante percebe.
As respostas são, portanto, segmentadas antes que qualquer coisa seja reescrita:
| Segmento | Tratamento |
| --- | --- |
| Prosa | Higienizada por Unicode, depois reescrita |
| Blocos cercados (``` e ~~~) | Caracteres invisíveis e bidi removidos. **Sem NFKC, nunca reescritos.** |
| Spans inline `` `code` `` | O mesmo |
Isso também vale para o caminho de streaming, onde uma cerca abre em um delta e fecha
vários deltas depois. Um delta que atravessa a fronteira é dividido por linha, de modo que o
fechamento ``` e a prosa após ele são tratados de forma diferente. Uma cerca não fechada falha
de forma segura: o restante é protegido em vez de reescrito.
Desative com `text.protect_code_blocks: false` se quiser o comportamento antigo.
### Arquivos de código-fonte: uma verificação de segurança, não uma remoção de marca d'água
**Não há marca d'água em código-fonte gerado por IA.** Nenhum provedor aplica marca d'água
na saída de código, e não existe detector público. Quem afirma remover uma está
vendendo algo para você.
O que o código-fonte *realmente* tem é uma superfície de ataque real e publicada:
* **Trojan Source** ([CVE-2021-42574](https://nvd.nist.gov/vuln/detail/CVE-2021-42574),
Boucher & Anderson 2021) — caracteres de controle bidirecionais reordenam como o código é
*exibido* sem alterar como ele *compila*. Um revisor aprova um programa; o
compilador constrói outro.
* **Identificadores homóglifos** ([CVE-2021-42694](https://nvd.nist.gov/vuln/detail/CVE-2021-42694))
— `а` cirílico no lugar do `a` latino cria dois nomes que são renderizados de forma idêntica.```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":
Após purgar, a linha fica if access_level != "user // Check if admin": — o
"comentário" esteve dentro da string o tempo todo.
Três coisas que o caminho de código deliberadamente não faz, porque o caminho de texto genérico fazia todas as três e cada uma é um bug no código-fonte:
"Hello" vira
"Hello", "office" vira "office". Isso altera o que um programa compara, aplica hash
e transmite.а cirílico mescla dois identificadores
que o compilador atualmente trata como distintos — mudando silenciosamente o comportamento. A severidade é
MEDIUM apenas para tokens de scripts mistos (totаl), a assinatura de ataque real; uma palavra
escrita inteiramente em outro script é texto estrangeiro comum e recebe INFO. Ative
com --fold-homoglyphs-in-code depois de revisá-los.inspect --json emite, para cada achado, severidade, linha, coluna e ponto de código, então ele se integra
à CI como um gate de pre-commit ou PR.
Removido, de forma verificável: o dicionário /Info (Author, Title, Subject, Keywords,
Creator, Producer, CreationDate, ModDate), o pacote XMP em /Root/Metadata, anexos
de arquivo incorporados e JavaScript em nível de documento. Páginas, texto e geometria de página são
preservados exatamente; a operação é idempotente e estável em bytes.
Detectados, mas NÃO removidos: caracteres invisíveis dentro dos fluxos de conteúdo das páginas. O PDF
desenha texto glifo a glifo por meio de uma codificação específica da fonte — um espaço de largura zero em uma
fonte com codificação CID é um índice de glifo de dois bytes, não um U+200B literal —, portanto, uma reescrita genérica
corromperia o layout em vez de limpá-lo. inspect informa a contagem; purge
imprime um aviso em vez de permanecer em silêncio, porque o silêncio seria interpretado como "não havia
nenhum". Para removê-los, extraia o texto, execute nullorigin purge nele e regenere
o PDF.
--strip-annotations está disponível, mas desativado por padrão: as anotações incluem links e
campos de formulário, não apenas comentários, portanto removê-las altera o comportamento do documento.
Travessões, aspas curvas e reticências são saída comum de processadores de texto. O NullOrigin
os preserva por padrão e os relata separadamente dos achados genuínos, porque
achatá-los degrada um documento sem higienizar nada. Use
--flatten-typography se você quiser especificamente uma saída ASCII.
Confusíveis entre scripts são diferentes — um о cirílico no meio de uma palavra em inglês não tem
uso legítimo — e esses são convertidos por padrão.
--verify relata medições antes/depois em vez de afirmar sucesso:```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 o backend estiver inacessível, isso é relatado como um aviso no stderr — um
fallback silencioso pareceria idêntico a uma sanitização bem-sucedida.
---
## 📊 Benchmarking```bash
nullorigin build-datasets
nullorigin benchmark
Cada valor é medido no local: o texto é marcado com KGWWatermarkEmbedder, executado
pelo pipeline real e reavaliado com o detector correspondente. O runner encerra
com código não zero quando os limiares não são atingidos e explica o motivo.
Limiares (da diretriz do projeto):
| Métrica | Alvo |
|---|---|
| z-score pós-sanitização | $\lvert z\rvert \le 1.5$ |
| Similaridade semântica | $\ge 0.92$ |
| SSIM da imagem | $\ge 0.95$ |
| PSNR da imagem | $\ge 36$ dB |
Seção de texto completo sobre datasets/text/watermarked_kgw.json, reescrita por meio
do Ollama (qwen3:4b) em um MacBook da série M (~150 s por passagem):```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)
Toda amostra com marca d'água passou de detectada para não detectada. Observe `kgw_003` em
z = 1.271 — abaixo do limiar, mas a mais próxima dele, que é a forma honesta deste
ataque: é estatístico, não uma garantia.
Mídia, medida nos fixtures de imagem:```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
Ambos os limiares de imagem passam (SSIM ≥ 0.95, PSNR ≥ 36 dB). Os números variam conforme o modelo, o hardware e o trecho.
Uma reescrita que altera um fato pontua exatamente como uma fiel na métrica de marca d'água. A verificação óbvia para isso não funciona, e a menos óbvia também não. Medido em seis casos de deriva mais um controle fiel:
A sobreposição lexical está invertida. Toda edição que destrói o significado pontuou mais alto do que a reescrita fiel, porque uma boa paráfrase compartilha poucos n-gramas com sua fonte, enquanto uma corrompida compartilha quase todos.
O cosseno do embedding não resolve isso. Três dos seis casos corrompidos ultrapassam um
limiar de 0,92. "Alice paid Bob" e "Bob paid Alice" são o mesmo saco de palavras e
pontuam 0,985; "must not disable" → "must disable" pontua 0,947. Os embeddings de frases
codificam relação temática, não verdade.
Portanto, a fidelidade é verificada em duas camadas, e nenhuma delas é o cosseno:
negation count changed: 1 → 0). Modais e quantificadores são comparados por
classe de significado, então may → might passa e may → must falha. Cega a trocas de
papéis em que todas as entidades sobrevivem.nullorigin[nli]; sem isso, a limitação é relatada, não ocultada.Medir a deriva depois do fato não ajuda se o texto danificado já foi retornado. Uma
verificação com falha tenta novamente a uma temperatura mais baixa — a deriva é
impulsionada pela temperatura — e, após o orçamento de tentativas, retorna o original,
com ok=False e o motivo.```yaml
text:
fidelity:
enabled: true
max_retries: 2
temperature_step: 0.25
use_nli: true
nli_threshold: 0.5
Isto também significa que o ataque e o risco partilham um único dial: aumentar a temperatura reduz o
z-score *e* aumenta a taxa de deriva. O benchmark reporta-os em conjunto, em vez de
verificações independentes.
### Outras métricas
* **Perplexidade** — PPL verdadeiro do GPT-2 com `torch` + `transformers`, caso contrário
`unigram_entropy_proxy`, marcado como aproximado e **não** comparável ao PPL publicado.
* **Similaridade de cosseno** ainda é reportada como `mean_cosine_or_lexical`, apenas para referência.
Deixou de ser um critério de aprovação/reprovação, pelas razões na tabela acima.
## ⚙️ Configuração
Ordem de resolução, da precedência mais baixa para a mais alta:
1. Predefinições incorporadas
2. `nullorigin.yaml` (procurado em `./`, `../`, `/app/` ou `$NULLORIGIN_CONFIG`)
3. Variáveis de ambiente `NULLORIGIN_*`
4. Flags explícitas de CLI
### Definições principais
| Definição | Predefinição | Notas |
| --- | --- | --- |
| `proxy.host` | `127.0.0.1` | Loopback. Uma vinculação pública é recusada sem um token de telemetria. |
| `proxy.port` | `8080` | |
| `proxy.default_provider` | `openai` | Usado quando nenhum cabeçalho `x-nullorigin-provider` é enviado. |
| `proxy.telemetry_token` | `""` | Protege `/telemetry` e `/telemetry/reset`. |
| `proxy.max_request_bytes` | `104857600` | 100 MiB; corpos maiores recebem `413`. |
| `text.paraphraser.backend` | `ollama` | `none` \| `ollama` \| `openai_compatible` \| `llama_cpp` \| `lexical`. `none` deixa a marca de água estatística intacta. `lexical` não precisa de modelo, mas é um ataque muito mais fraco. |
| `text.clean_unicode` | `true` | Remoção de zero-width e do bloco Tags. |
| `text.fold_homoglyphs` | `true` | Confundíveis cirílicos/gregos para ASCII. |
| `text.stream_window_tokens` | `40` | Deltas em buffer antes de um span de streaming ser reescrito. |
| `media.crop_mode` | `trim` | `trim` desloca as coordenadas sem reamostragem; `resample` restaura as dimensões exatas, mas custa aproximadamente SSIM 0.81 / PSNR 31 dB mesmo com um corte de 0.5%; `none` desativa a passada geométrica. |
| `audio.low_cut_hz` | `800.0` | A fase abaixo deste valor é preservada para inteligibilidade. |
### Variáveis de 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
O modelo configurado não foi baixado. Execute ollama list para ver o que você tem e, em seguida, use
ollama pull llama3.2:3b ou defina NULLORIGIN_PARAPHRASER_MODEL para um modelo que você já
possua.
WARNING: ollama backend unavailable (ReadTimeout)
A reescrita excedeu text.paraphraser.timeout_seconds (padrão 120 s). Modelos de raciocínio
como qwen3 costumam levar 150 s+ por parágrafo na CPU. Aumente:
export NULLORIGIN_PARAPHRASER_TIMEOUT=900 ou use um modelo de instrução menor.
nullorigin benchmark sai com código 1 e pass_no_detection: FAIL
Funciona como esperado. Nenhum backend de reescrita estava acessível, então apenas a camada unicode foi executada e
a marca d'água estatística sobreviveu. Inicie o Ollama ou defina o backend para lexical para uma
comparação sem dependências.
semantic_check: INCONCLUSIVE
Esperado sem o extra [metrics]. Veja Honestidade das métricas.
Error: Refusing to bind 0.0.0.0 without a telemetry token
Intencional. Veja Implantação além do localhost.
Multiple top-level packages discovered in a flat-layout
Você está em um checkout antigo. pyproject.toml define uma lista explícita de pacotes; atualize para a versão mais recente.
Testes assíncronos relatam UsageError sobre um plugin assíncrono ausente
Deliberado — sem um, o pytest relata testes async def como aprovados sem aguardá-los.
Execute pip install -e ".[dev]".
Docker: curl: (7) Failed to connect logo após compose up
O healthcheck tem um período inicial de 10 s. Aguarde e então verifique 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 testes. A suíte cobre o parser de frames SSE contra limites de chunk adversariais, proxy
ciclo de vida de streaming, todos os modos de imagem PIL, SSIM contra tanto um valor de forma fechada quanto uma
implementação de referência por força bruta, e a detectabilidade de marca d'água em conjuntos de dados.
Testes assíncronos falham ruidosamente se nenhum plugin assíncrono estiver instalado, em vez de pular silenciosamente.
---
## 📖 Citação e reutilização
Licenciado sob Apache-2.0, que permite uso, modificação e redistribuição desde que
o aviso de direitos autorais e a atribuição a **Muhammad Rakibul Islam** sejam mantidos. Consulte
[LICENSE](https://github.com/rakib-nyc/nullorigin/blob/HEAD/LICENSE) e [NOTICE](https://github.com/rakib-nyc/nullorigin/blob/HEAD/NOTICE).
Se este trabalho apoiar uma publicação, cite-o como:```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}
}
Este repositório é publicado como um artefato de pesquisa finalizado e não está aceitando pull requests. Você é livre para fazer um fork sob os termos da licença. Perguntas e descobertas são bem-vindas por e-mail em [email protected].
Versão 1.0.0. A suíte está completa conforme sua especificação e totalmente testada, com estes limites conhecidos:
[nli], trocas de papéis que preservam todas as entidades são indetectáveis, e o relatório afirma isso.Consulte CHANGELOG.md para o histórico de versões.
Este repositório é um artefato de nível de pesquisa lançado para pesquisa estatística, avaliação de privacidade, benchmarking de robustez de marcas d'água e testes de resiliência criptográfica.```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.
**SEM GARANTIAS.** O SOFTWARE É FORNECIDO "NO ESTADO EM QUE SE ENCONTRA", SEM GARANTIA DE QUALQUER TIPO,
EXPRESSA OU IMPLÍCITA.
| Camada | O que ela realmente faz |
|---|
| Caracteres invisíveis | Totalmente eficaz. Cargas de zero-width, controle bidirecional, seletor de variação e bloco de tags Unicode são removidas completamente, com uma contagem reportada. Confusáveis homoglifos entre escritas (renderização cirílica/grega como ASCII) são normalizados. |
| Metadados de documento (.docx) | Totalmente eficaz. Autor, último editor, contagem de revisões, carimbos de data/hora, modelo e versão do aplicativo são limpos de docProps, com a formatação preservada byte por byte. |
| C2PA / EXIF / XMP | Totalmente eficaz. A imagem é reconstruída a partir de amostras de pixels brutos em um contêiner novo, portanto manifestos JUMBF assinados e todos os metadados são removidos. Verificado por teste contra fixtures marcados. |
| Marca d'água estatística KGW | Depende inteiramente do backend de reescrita. Sem um modelo local configurado, a marca d'água estatística sobrevive — a ferramenta afirma isso em vez de dar a entender o contrário. |
| SynthID-Text / SynthID-Image / Tree-Ring | Não verificável aqui. Esses usam chaves privadas e decodificadores proprietários. O NullOrigin aplica as perturbações que a literatura descreve, mas não é feita nenhuma afirmação de que eles derrotam os detectores reais, porque não há detector público para medir. |
| AudioSeal / SynthID-Audio | Não verificável aqui, pela mesma razão. |
| Opcional | Docker 20.10+ com Compose v2 |
| caso | lexical_f1 | embedding cosine | bidirectional NLI |
|---|
| negação removida | 0.70 | 0.77 ✓ | 0.000 ✓ |
| número 5 → 50 | 0.82 | 0.81 ✓ | 0.000 ✓ |
| troca de entidade/papel | 0.81 | 0.985 ✗ | 0.000 ✓ |
| quantificador todos → alguns | 0.88 | 0.91 ✓ | 0.000 ✓ |
| hedge removido | 0.27 | 0.953 ✗ | 0.011 ✓ |
| "must not" → "must" | 0.83 | 0.947 ✗ | 0.000 ✓ |
| reescrita fiel | 0.33 | 0.931 ✓ | 0.998 ✓ |