> ## Documentation Index
> Fetch the complete documentation index at: https://docs.watchdoc.sphinxhq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Client-side redaction

> Redact PII on the user's device before upload so Watchdoc and Google never receive the real values.

Optional **redact before upload** replaces structured PII with consistent placeholder tokens
on the client's machine. Watchdoc only ever receives the redacted file plus a content-free
forensic sidecar. The playground does this in the browser; API users can do the same on
their side and tell us via `redacted` + `redaction_sidecar`.

## What gets redacted automatically

| Detected                                                    | Token example              |
| ----------------------------------------------------------- | -------------------------- |
| Credit / debit cards (Luhn-validated)                       | `[CARD_1]`                 |
| US SSNs                                                     | `[SSN_1]`                  |
| Emails                                                      | `[EMAIL_1]`                |
| Phone numbers                                               | `[PHONE_1]`                |
| IBANs                                                       | `[IBAN_1]`                 |
| Bank account / routing numbers (context-gated)              | `[ACCT_1]` / `[ROUTING_1]` |
| Person names (label-anchored regex + on-device NER)         | `[NAME_1]`                 |
| Street addresses (regex + on-device NER)                    | `[ADDR_1]`                 |
| Handwritten / stamped signatures (ink near labels or names) | `[SIG_1]`                  |

Detection patterns for structured identifiers are ported from
[Microsoft Presidio](https://github.com/microsoft/presidio) (MIT). Contextual
names and street lines are also scored by [Rampart](https://github.com/nationaldesignstudio/rampart)
(CC BY 4.0) — a \~15 MB MiniLM NER model that runs entirely in the browser via
WASM. Monetary amounts, dates, BIC/SWIFT codes, and ISO currency codes
are kept so balance-math and structural fraud signals still work. The playground
preview still lets the user drag boxes over anything the detectors missed.

## Token semantics

* Same original value → same token everywhere (`1234-5678` and `1234 5678` share `[ACCT_1]`).
* Different values → different indices (`[NAME_1]` vs `[NAME_2]` is a real inconsistency).
* The engine is told these rules and will not treat placeholders themselves as fraud.

## Coverage tradeoffs

**Kept**

* Content checks on non-redacted text
* Cross-field consistency via tokens
* Font forensics on remaining text
* Structural signals via the sidecar (version count, editors, font hints)
* Incremental-save history: each save of the original is analyzed locally with the same
  token registry, so a PII value edited between saves surfaces as a token diff
  (`[NAME_2]` removed, `[NAME_1]` added) without shipping old bytes
* Incremental-edit content: text spans added by later saves (edited amounts, dates)
  are diffed locally and shipped with PII tokenized — non-PII edits like value changes
  stay verbatim, so "the net pay was edited after creation" still reaches the engine
* Editing-tool fingerprints per save, scraped from each save's appended raw bytes
  (catches e.g. an Adobe touch on a Chrome-made PDF whose parsed metadata still
  claims the original producer)
* The playground X-ray viewer: each incremental save is rendered and redacted
  on-device (same tokens, same main-pass coverage including OCR hits and
  signatures, same manual boxes) and uploaded alongside the final document,
  together with a copy of the final pages with changed regions boxed in red —
  computed with the same last-two-saves cardinality diff the server runs on
  unredacted uploads — so the version slider and "pages flagged" navigation
  work identically, without any original bytes leaving the device
* Visual / AI analysis outside replaced regions
* Paste-detection features at replaced regions as numeric pixel forensics
  (noise / background / sharpness / ELA / baseline deltas)
* Provenance verdicts the engine normally reads off the raw file, which flattening
  destroys: C2PA AI-generation markers (PDF info/XMP and DCT-embedded full-page
  images), the Photoshop-authored JPEG marker, and the empty-shell revision
  structure (see `ai_provenance`, `embedded_ai_provenance`, `photoshop_saved`,
  `text_layer`). These decide the result on their own, so omitting them from the
  sidecar means an AI-generated or Photoshop-authored document can come back clear.
  FlateDecode-compressed embedded images are a residual gap — the client cannot
  inflate them in-browser, so only DCT-embedded JPEGs are scanned.

**Lost**

* Open-ended visual judgment of the replaced patches beyond those features
* Semantic checks on the redacted values themselves
* Edit-history analysis beyond what the sidecar captured

OCR can miss on poor scans — the preview (and manual boxes) is the last line of defense.

## Playground

On the Documents upload modal, enable **Redact before upload**. Each file is OCR'd and
redacted locally; you preview the result (and can drag extra boxes) before anything is sent.

For PDFs with incremental saves, the browser also uploads a redacted render of each save
plus a changed-region highlight of the final pages (`redaction_versions_N` /
`redaction_highlight_N` form fields), which power the X-ray version slider on the result
page. Every covered region from the main pass (text-layer hits, OCR-only hits, signatures,
and manual boxes) is re-projected onto those historical renders so scanned PII and ink
cannot leave the device under the "Previous save" side.

## API: redacting on your side

No SDK is required. Redact locally, then submit the redacted bytes with:

```json theme={null}
{
  "file": "<multipart>",
  "redacted": true,
  "redaction_sidecar": { "...": "see schema below" }
}
```

`redacted: true` alone is not enough to activate the engine's redaction-artifact
suppression (the prompt addendum that tells the model to ignore black boxes,
overlays, crisp placeholder labels, etc.). The server only emits that addendum when
extracted content contains `[PREFIX_N]` placeholders, or the sidecar's
`pixel_forensics` inventory lists covered regions with both a `token` and `page`.
A bare `tokens` list (or a bare flag with an empty sidecar) is not verification —
visual fraud cues stay valid.

Recommended open-source tooling for server-side redaction:

* [Microsoft Presidio](https://github.com/microsoft/presidio) — detection + anonymization
  to typed placeholders
* A PDF renderer (e.g. pypdfium2) if you need to flatten scanned pages safely

### Token format

Placeholders must look like `[PREFIX_N]` where `PREFIX` is one of
`NAME`, `ACCT`, `ROUTING`, `CARD`, `SSN`, `EMAIL`, `PHONE`, `IBAN`, `ADDR`, `ID`, `MRZ`
(or your own), and `N` is a stable 1-based index for that prefix. Same original
value → same token — including across saves in `pdf.versions`, which is what makes
`version_diffs` meaningful.

### Sidecar schema (all fields optional)

```json theme={null}
{
  "version": 2,
  "kind": "pdf",
  "tokens": [{ "token": "[ACCT_1]", "type": "US_BANK_NUMBER" }],
  "pdf": {
    "version_count": 2,
    "producer": "Acrobat Distiller",
    "creator": "Microsoft Word",
    "creation_date": "D:20240101120000Z",
    "mod_date": "D:20240105120000Z",
    "eof_count": 2,
    "ai_provenance": null,
    "embedded_ai_provenance": null,
    "final_span_count": 408,
    "text_layer": {
      "raster_pages": 0,
      "visible_text_ops": 0,
      "hidden_text_ops": 0
    },
    "total_versions": 2,
    "analyzed_versions": 2,
    "versions": [
      {
        "index": 1,
        "byte_length": 91230,
        "producer": "Microsoft Word",
        "creator": null,
        "creation_date": "D:20240101120000Z",
        "mod_date": null,
        "page_count": 3,
        "object_count": 41,
        "annotation_count": 0,
        "has_javascript": false,
        "has_embedded_files": false,
        "has_acroform": false,
        "has_signature": false,
        "tokens": ["[NAME_2]", "[ACCT_1]"]
      },
      {
        "index": 2,
        "byte_length": 103450,
        "producer": "Adobe Photoshop",
        "creator": null,
        "creation_date": "D:20240101120000Z",
        "mod_date": "D:20240105120000Z",
        "page_count": 3,
        "object_count": 45,
        "annotation_count": 0,
        "has_javascript": false,
        "has_embedded_files": false,
        "has_acroform": false,
        "has_signature": false,
        "tokens": ["[NAME_1]", "[ACCT_1]"],
        "tool_hints": ["MuPDF"],
        "segment_writer": "MuPDF 1.27.1",
        "segment_producer": null,
        "segment_creator": null,
        "segment_xmp_tool": null
      }
    ],
    "version_diffs": [
      {
        "from": 1,
        "to": 2,
        "removed_tokens": ["[NAME_2]"],
        "added_tokens": ["[NAME_1]"]
      }
    ],
    "incremental": {
      "base_version": 1,
      "final_version": 2,
      "new_texts": [": 31/01/2026", "8,200.00", "[NAME_1]"],
      "new_span_count": 5,
      "final_span_count": 408,
      "modification_ratio": 0.012
    }
  },
  "image": {
    "format": "jpeg",
    "software": null,
    "editor_hints": ["EXIF Software = 'Adobe Photoshop 24.0'"],
    "camera_make": "Apple",
    "camera_model": "iPhone 14",
    "capture_time": "2024:01:01 10:00:00",
    "lens_model": null,
    "ai_provenance": null,
    "photoshop_saved": false
  },
  "fonts": {
    "page_fonts": ["Helvetica", "Arial"],
    "replaced": [
      {
        "token": "[NAME_1]",
        "replaced_font": "ComicSansMS",
        "surround_fonts": ["Helvetica"]
      }
    ]
  },
  "pixel_forensics": [
    {
      "token": "[NAME_1]",
      "page": 1,
      "ela_residual": 4.4,
      "baseline_offset": 1.2
    }
  ]
}
```

Harvest the sidecar from the **original** file before you flatten/redact. Never include
raw PII in the sidecar. Incremental-edit `new_texts` are scrubbed for known registry
values (≥2 characters) before they leave the device; a residual gap remains for PII that
existed only in a historical save and was never detected on the final document (so it
never entered the registry).

### Provenance fields

These fields carry verdicts the engine would otherwise read off the raw bytes. Flattening
destroys all of them, so if you omit them a document the engine would have rejected
outright reaches the model as an ordinary picture and can come back clear.

| Field                        | Set it when                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `image.ai_provenance`        | The original's metadata carries a C2PA AI-generation marker — `trainedAlgorithmicMedia`, `compositeWithTrainedAlgorithmicMedia`, or Google's C2PA watermark trio. Scan PNG `tEXt`/`iTXt`/`eXIf`/`caBX` chunks and JPEG `APP`/`COM` segments. Send a short description of where you found it.                                                                                                                                               |
| `pdf.ai_provenance`          | Same markers in the PDF info dictionary or XMP packet.                                                                                                                                                                                                                                                                                                                                                                                     |
| `pdf.embedded_ai_provenance` | Same markers inside a DCT-embedded JPEG large enough to be a full-page image (\~400KB+). Small AI-generated logos must not convict. FlateDecode-compressed embeds are not visible to a byte scan.                                                                                                                                                                                                                                          |
| `image.photoshop_saved`      | The JPEG's APP13 `Photoshop 3.0` payload contains 8BIM resource `0x0406`. Do **not** set it merely because an APP13 block exists — that container is standard IPTC that any tool writes, and flagging on it produced \~2.2% false positives. Strict boolean `true` only.                                                                                                                                                                   |
| `pdf.text_layer`             | Always, for PDFs. Count, over the first 10 pages, how many pages paint an image covering more than half the page (`raster_pages`), and split the text-showing operators into those drawn after the last such image (`visible_text_ops`) versus before it or in invisible render mode 3 (`hidden_text_ops`). Visible-over-raster text means values were stamped onto a picture of a document; an OCR layer is invisible or behind the scan. |

`pdf.text_layer` only convicts alongside `eof_count > 1`, every earlier revision in
`versions` reporting `page_count` 0 (or failing to parse), `final_span_count` of at least
20, and 48h or more between `creation_date` and `mod_date`.

### Response

`serialize_check` exposes `document.redacted: true` so consumers know coverage was
reduced. Extracted fields may contain tokens, e.g. `"name": "[NAME_1]"`.

## Attribution

* Structured-PII recognizers adapted from [Microsoft Presidio](https://github.com/microsoft/presidio) (MIT).
* Contextual name/address NER: [Rampart](https://github.com/nationaldesignstudio/rampart) by National Design Studio (CC BY 4.0). Model weights are self-hosted under `/static/vendor/rampart/`.
