Metadata-Version: 2.4
Name: owmark
Version: 0.8.1
Summary: Layered post-hoc text watermark + generation-time watermark for marking and recognising your own text
Author-email: Marko Jukic <jukic.marko@gmail.com>
License-Expression: GPL-3.0-or-later
Project-URL: Homepage, https://gitlab.com/Jukic/owmark
Project-URL: Repository, https://gitlab.com/Jukic/owmark
Project-URL: Changelog, https://gitlab.com/Jukic/owmark/-/blob/main/CHANGELOG.md
Project-URL: Issues, https://gitlab.com/Jukic/owmark/-/issues
Keywords: watermark,text,provenance,steganography,llm,c2pa,multilingual,slovenian
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Text Processing :: Linguistic
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: crypto
Requires-Dist: cryptography>=41; extra == "crypto"
Provides-Extra: hf
Requires-Dist: transformers>=4.40; extra == "hf"
Requires-Dist: torch; extra == "hf"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: ruff>=0.6; extra == "dev"
Requires-Dist: mypy>=1.10; extra == "dev"
Dynamic: license-file

<p align="center"><img src="https://gitlab.com/Jukic/owmark/-/raw/main/owmark_large_logo.png" alt="OWMark logo" width="160"></p>

# OWMark

Mark your own text so you can **recognise it later**, and watermark **model output**
at generation time. A hardened, installable implementation of the layered post-hoc
design.

**New to OWMark?** The [plain-language guide and step-by-step tutorial](https://jukic.gitlab.io/owmark/)
explain how it works and how to use it, with no programming needed (source: [`docs/guide/`](https://gitlab.com/Jukic/owmark/-/tree/main/docs/guide)).


> **Design honesty.** Strong, un-removable text watermarking is provably impossible
> against a quality-preserving paraphraser (Zhang et al., ICML 2024). OWMark therefore
> locates its one robust guarantee — **unforgeable provenance** — in cryptography (an
> Ed25519 signature over the canonicalised text), while the in-text layers are
> deliberately cheap and removable: they provide traceability and tamper-evidence rather
> than robustness. Every verification returns a **calibrated p-value**, not a bare
> yes/no.

> **New here?** Start with the **[tutorial](https://gitlab.com/Jukic/owmark/-/blob/main/TUTORIAL.md)** — a hands-on walkthrough
> (embed, verify, tamper-detection, CLI, and multilingual use) with runnable examples.

## Install

```bash
pip install owmark              # core (pure stdlib, fully functional)
pip install "owmark[crypto]"    # + real Ed25519 provenance signatures
pip install "owmark[hf]"        # + Hugging Face logits processor
```

From a clone of the [repository](https://gitlab.com/Jukic/owmark): `pip install .`, or
`pip install -e ".[dev]" && pytest -q` to work on OWMark itself.

## Two ways to use it

### 1. Post-hoc — mark text you already have

```python
from owmark import Keys, embed, verify

keys = Keys.generate()
keys.save("my.keys.json")                   # your secret: owner-only file, back it up
marked, cred = embed(my_text, keys, author_id="mjukic", work_id="report-01")
v = verify(marked, keys, cred)
print(v.in_text, v.provenance, f"{v.p_value:.1e}")
# -> OURS PROVEN 2.3e-10       (in_text needs a few hundred words; provenance does not)
```

Need an **in-text, multi-bit author/work identifier** (not just presence)? Use payload mode
— a BCH code + keyed MAC with a calibrated `2**-(MAC bits)` false-positive:

```python
from owmark import embed_payload, read_payload

marked = embed_payload(my_text, keys, author_id=42, work_id=7, version=1, profile="full")
pv = read_payload(marked, keys, profile="full")
print(pv.decoded, pv.author_id, pv.work_id, pv.version)   # -> True 42 7 1
```

CLI:

```bash
owmark keygen -o my.keys.json          # owner-only file; never overwrites an existing key
owmark embed  --keys my.keys.json --author mjukic --work report-01 --in draft.txt --out marked.txt
owmark verify --keys my.keys.json --in marked.txt --cred marked.txt.owc   # --json for scripts
owmark peek   --in marked.txt          # key-free Layer C fingerprint (exit 1 if none)
owmark embed-id --keys my.keys.json --author-id 42 --work-id 7 --in draft.txt --out id.txt
owmark read-id  --keys my.keys.json --in id.txt      # -> identifier  author 42, work 7, ...
```

**Proving it to someone else.** Your key file is secret, but provenance does not need it to
be checked: publish your **public key** and anyone — a publisher, a reviewer, a court — can
verify your credentials independently, without being able to forge any (needs `owmark[crypto]`):

```bash
owmark pubkey --keys my.keys.json > mjukic.pub                     # safe to publish
owmark verify --pub mjukic.pub --in marked.txt --cred marked.txt.owc   # no secret involved
```

In Python: `keys.public_key()` and `verify_provenance(text, cred, public_key)`.

### 2. Generation-time — encode at model output

* **Local Hugging Face model** (true logit watermark):

  ```python
  from owmark.genmark import GenConfig, OWMarkLogitsProcessor, green_red
  cfg = GenConfig(key=b"secret", gamma=0.25, delta=2.0)
  out = model.generate(**inputs, logits_processor=[OWMarkLogitsProcessor(cfg)])
  n = inputs["input_ids"].shape[1]
  green_red.detect(out[0, n - 1:].tolist(), cfg)   # exact binomial p-value
  ```

  (Detect on the generated part — the prompt carries no watermark. The processor caches each
  previous token's green mask, so the overhead is a few ms per token.)

* **API / hosted model** (no logit access): mark the returned text post-hoc:

  ```python
  from owmark import Keys
  from owmark.genmark import OutputWatermarker
  wm = OutputWatermarker(Keys.generate(), "my-service", "gpt-proxy")
  text, cred = wm.mark(model_response)
  ```

## From other programs and AI agents

The command-line tool and the Python library above are two of four ways to use OWMark. The
other two run the same operations, with the same validation and results, and need no extra
packages.

### HTTP API

```bash
export OWMARK_API_TOKEN="$(python -c 'import secrets; print(secrets.token_urlsafe(32))')"
owmark serve --keys my.keys.json            # http://127.0.0.1:8765, this machine only
curl -s localhost:8765/v1/embed -H "Authorization: Bearer $OWMARK_API_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"text": "...", "author": "mjukic", "work": "report-01"}'
```

| Endpoint | Does | Token |
|---|---|---|
| `POST /v1/embed` | mark a text; returns the marked text and its credential | yes |
| `POST /v1/verify` | check a text (and its credential) with the server's key | yes |
| `POST /v1/verify-provenance` | check a credential with a public key | no |
| `POST /v1/peek` | look for the invisible seal | no |
| `POST /v1/fingerprint` | work fingerprint of an author and work | no |
| `POST /v1/embed-id`, `/v1/read-id` | hide / read a numeric identifier | yes |
| `GET /v1/info`, `/v1/public-key`, `/openapi.json` | server info, public key, API description | no |

The token is required whenever one is set (`--token` or `OWMARK_API_TOKEN`). The server refuses
to listen beyond 127.0.0.1 with a key file but no token. For access from other machines, put it
behind a reverse proxy with TLS. Errors are JSON: `{"error": {"code": ..., "message": ...}}`.

### MCP server for AI agents

`owmark mcp` speaks the [Model Context Protocol](https://modelcontextprotocol.io) over stdio, so
an AI assistant can mark and check texts with your key. Add it to your MCP client's
configuration; most clients use this form:

```json
{
  "mcpServers": {
    "owmark": {"command": "owmark", "args": ["mcp", "--keys", "/home/me/my.keys.json"]}
  }
}
```

In Claude Code: `claude mcp add owmark -- owmark mcp --keys /home/me/my.keys.json`.

- **Tools:** `embed`, `verify`, `verify_provenance`, `peek`, `fingerprint`, `embed_id`,
  `read_id`, `public_key` and `info`.
- **The key:** the key file is fixed when the server starts, and the agent never sees the secret.
  Without `--keys`, only the key-free tools are offered.
- **Paths:** use full paths. Desktop apps often do not see your shell's `PATH`; run `which owmark`
  (or `where owmark` on Windows) to get the full path to the command.

## How it works (three layers)

| Layer | Mechanism | Survives | Role |
|-------|-----------|----------|------|
| **A. Provenance** | Ed25519 signature over canonicalised text (+ RFC 6962 Merkle `Registry` with inclusion/consistency proofs) | reformatting, copy-paste; **unforgeable** | the legal/forensic core |
| **B. Statistical-lexical** | keyed synonym choice; **presence → binomial p-value**, or **payload → BCH+MAC multi-bit ID** | copy-paste, light edits | traceability of excerpts |
| **C. Fragile zero-width** | invisible signature fingerprint | nothing (one-line strip) | instant check + **tamper signal** |

`Verdict` (from `verify`) reports `provenance` (`PROVEN`/`FOREIGN`/`BROKEN`/`UNVERIFIABLE`/
`UNKNOWN`), `in_text` (with `p_value` and the number of `carriers` it rests on), the
**key-free** `fragile_fingerprint`, `fragile_present`, and `tamper_suspected` (B/A verify but
our C marker is gone or replaced → laundering). `verify_provenance` (public key only) returns a
`ProvenanceVerdict` with the same Layer A and C fields.
`owmark.peek(text)` reads the key-free fingerprint on its own — an instant, no-secret
"is this marked, and which work?" check. The keyed multi-bit identifier is read separately
with `read_payload` (payload mode).

**Docs:** [tutorial](https://gitlab.com/Jukic/owmark/-/blob/main/TUTORIAL.md) · [design & algorithms](https://gitlab.com/Jukic/owmark/-/blob/main/DESIGN.md) · [changelog](https://gitlab.com/Jukic/owmark/-/blob/main/CHANGELOG.md).

## Limits (state these in any deployment)
- Layers B and C **do not survive paraphrase or retyping** — by design and by theorem.
- The robust guarantee is Layer A; its strength depends on **key management** and the
  registry's timestamping. Lose the key and you can no longer prove anything marked with it;
  leak it and others can forge your marks. Third parties can check Layers A and C with your
  public key, but the in-text Layer B mark is checkable only with the secret.
- The default synonym tables are **general-prose** (**English and Slovenian ship**; pass
  `language="slovenian"`); tune them for scientific text before serious use, or define your
  own with `owmark.Language(name, pairs)` (each word may belong to one pair only).
- OWMark marks *your* text. It does **not** reliably detect *others'* AI text — that
  (Q1 detectors) is the unreliable problem discussed in the paper.

## Reproducing the paper

The `evaluation/` directory of the [repository](https://gitlab.com/Jukic/owmark) contains the
full evaluation harness — every experiment behind the paper's figures and tables, each writing
a machine-readable `results/<TEST>/result.json`.

```bash
cd evaluation
python3 fetch_corpora.py     # one-time: pull the public-domain corpora (Gutenberg/Wikipedia)
./run_all.sh stdlib          # calibration, power, robustness, capacity, density, throughput
./run_all.sh ml              # neural collapse, MAUVE, head-to-head (needs a uv venv + GPU)
```

The harness runs the **frozen research snapshot** the paper was produced with
(`evaluation/owmark_v03/`, `evaluation/prototype/`), not the installed package, so it reproduces the
published numbers exactly; the package has evolved since (see the changelog).
Paths are repo-relative, so the harness runs wherever you clone it. See
[`evaluation/REPRODUCE.md`](https://gitlab.com/Jukic/owmark/-/blob/main/evaluation/REPRODUCE.md) for the full manual. The corpora are
downloaded (not committed); the small `result.json` outputs are included for reference.

## License

OWMark is released under the **GNU General Public License v3** (see [`LICENSE`](https://gitlab.com/Jukic/owmark/-/blob/main/LICENSE)).
```
