Skip to content

stella/anonymize

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

142 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

stella anonymize

Anonymization pipeline for sensitive text. Deterministic, local-first, fast.

Website · Issues · npm · PyPI · Discord

npm PyPI CI License: Apache-2.0 Discord


A single Rust core does the PII detection, resolution, and replacement; thin Node.js, browser (WebAssembly), Python, and CLI bindings translate types and call into it. There is no model server and no network dependency: the same document produces the same redaction on every run and in every runtime. It is built for legal documents (contracts, filings, correspondence) across a dozen languages, and it is open source under the Apache-2.0 license.

Quickstart

Node.js

npm install @stll/anonymize

still resolves to the stable 1.x line until 2.0.0 is promoted to latest.

import { getDefaultNativePipeline, deanonymise } from "@stll/anonymize";

const anonymize = getDefaultNativePipeline({ language: "en" });
const { redaction } = anonymize.redactText(
  "Contact Jan Novák at jan.novak@example.com.",
);

console.log(redaction.redactedText);
// Contact [PERSON_1] at [EMAIL_ADDRESS_1].
console.log(deanonymise(redaction.redactedText, redaction.redactionMap));
// Contact Jan Novák at jan.novak@example.com.

Create the pipeline once at startup and reuse it; it caches the prepared package and search automata. If you know the document language, pass it so the runtime loads the smaller scoped artifact — scoped packages ship for cs, de, and en (other supported languages use the full default package; requesting a scoped package that is not bundled fails at load).

Browser (Vite + WebAssembly)

npm install @stll/anonymize-wasm

The wasm build exposes the same SDK surface, backed by WebAssembly. Register the Vite plugin so the wasm binary, WASI worker, and .stlanonpkg assets survive dependency pre-bundling and production builds. The packages option controls which prepared packages ship (the full-dictionary default is ~20 MB, so most apps restrict it):

// vite.config.ts
import stllAnonymizeWasm from "@stll/anonymize-wasm/vite";

export default {
  // Emit only the packages the app loads, e.g. English + Czech.
  plugins: [stllAnonymizeWasm({ packages: ["en", "cs"] })],
};
import { loadDefaultPipeline } from "@stll/anonymize-wasm";

const pipeline = await loadDefaultPipeline("en");
const { redaction } = pipeline.redactText("A contract signed by Jan Novák.");
console.log(redaction.redactedText);

The binding targets wasm32-wasip1-threads (shared memory), so it needs a cross-origin-isolated (SharedArrayBuffer) context. See packages/anonymize/wasm/README.md for the full packages option reference.

Python

Prebuilt wheels (available from the 2.0.0 release) bundle the native pipeline packages, so no monorepo checkout is required.

uv add stella-anonymize-core
# or: pip install stella-anonymize-core
import stella_anonymize as anonymize

prepared = anonymize.preload_default_native_pipeline(language="en")
result = prepared.redact_text("Contact Jan Novák at jan.novak@example.com.")

print(result.redaction.redacted_text)
# Contact [PERSON_1] at [EMAIL_ADDRESS_1].

The Python SDK uses the same Rust core and prepared-package contract as the Node SDK. See crates/anonymize-py/README.md.

CLI

No install needed:

echo "Contact Jan Novák at jan.novak@example.com" | bunx @stll/anonymize-cli
# Contact [PERSON_1] at [EMAIL_ADDRESS_1]

Batch a directory in reversible replace mode, then selectively restore one entity from the redaction key:

# Redact a document, writing the reversible key alongside it.
anonymize -k contract.key.json -o contract.anon.txt contract.txt

# Restore only the person; every other placeholder stays redacted.
anonymize -d contract.key.json --revert "[PERSON_1]" contract.anon.txt

# Recursively anonymize a tree, 8 files in flight, mirroring into out/.
anonymize --recursive --workers 8 -o out/ docs/

--revert is repeatable and matches either a placeholder token ([PERSON_1]) or an original value (Jan Novák), case-sensitive and exact. All processing is local; the CLI makes no network calls. Run anonymize --help for the full reference, including the --json schema and exit codes.

Features

  • 22 default entity labels, plus 3 opt-in network labels. People, organizations, addresses, countries, and land parcels; email, phone, dates, and dates of birth; and a family of identifiers: IBAN and bank account numbers, tax and national identification numbers, identity card, birth, social security, passport, and registration numbers, credit card numbers, crypto addresses, and monetary amounts. IP addresses, MAC addresses, and URLs are built in but opt-in. The versioned machine-readable contract is exported as CAPABILITY_MANIFEST from @stll/anonymize/capabilities and printed by anonymize --capabilities; scope detection to a subset with --labels.
  • 12 languages, multi-script name corpora. Built-in coverage for cs, de, en, es, fr, hu, it, pl, pt-br, ro, sk, and sv, backed by name corpora that reach beyond Latin script (CJK, Arabic, Thai, Korean, and romanized variants).
  • Deterministic numbered placeholders with coreference linking. Each entity gets a stable [LABEL_N] placeholder; repeated and coreferent mentions (for example a defined term and its later short form) collapse to the same number, so the same input always yields the same output.
  • Reversible keys and selective revert. replace mode emits a self-describing redaction key; deanonymisation restores the original text, and the CLI can revert a chosen subset while leaving the rest redacted.
  • Extensible detection. Layer in your own exact-match deny lists, gazetteer entries, and deterministic custom regexes; caller-owned data is baked into the prepared package.
  • Streaming and diagnostics APIs. Beyond redactText, the SDK exposes JSON, streaming, and per-entity diagnostics variants for pipelines that need spans, scores, and detection provenance.
  • Offline CLI. Reads files or stdin, processes directories in parallel, and never makes a network call.

Benchmarks

@stll/anonymize compared against three open-source PII libraries on a public, synthetic, legal-domain corpus (en/cs/de: 28 documents, 196 gold entities). Matching is span-overlap (same label, IoU >= 0.5). Full numbers, per-label tables, and methodology live in packages/benchmark; this run is from packages/benchmark/results/latest.md.

Library Version F1 (overlap, all labels) Throughput (warm, chars/s)
stella 2.0.0 83.4 907,102
presidio 2.2.360 50.9 41,244
redact-pii 3.4.0 31.6 55,823
scrubadub 2.0.1 26.2 1,563,700

Per-language overlap F1 (all labels):

Library cs de en
stella 77.2 81.6 90.5
presidio 35.0 54.7 60.5
redact-pii 24.8 34.8 34.8
scrubadub 20.3 31.7 27.6

Read these numbers with their caveats:

  • This is one fixture set. The corpus is legal-domain and multilingual (en/cs/de), which is exactly what stella is built for and skews the comparison toward it. scrubadub and redact-pii are English-only; their cs/de scores are expected to be low and are reported as-is. The claims here are scoped to this benchmark, not to PII redaction in general.
  • Synthetic ground truth. All text is public-safe synthetic legal prose with positionally-authored spans, so absolute numbers may differ from production filings. Every library sees identical inputs.
  • Competitors win in places. Presidio leads on organizations (56.3 vs. 51.0 F1) and on phone recall (84.6% vs. 46.2%); scrubadub and redact-pii edge email recall. Every label is reported, including where stella loses.

To try your own documents, packages/benchmark supports an --input mode; see REPRODUCING.md for the exact toolchain, library versions, and taxonomy-mapping decisions.

Architecture

One Rust core (crates/anonymize-core) owns detection, resolution, and config assembly. The Node.js, browser, and Python bindings are thin: they load a prepared package, translate types, and call the same core, so they produce identical structured output. That equivalence is enforced by cross-runtime parity tests in CI (python-parity.test.ts, the native SDK contract tests, and Rust adapter parity examples).

Dictionaries and language data are baked into .stlanonpkg prepared packages at build time, not loaded from the network at runtime. Native Node binaries ship as per-platform prebuilt sidecars (for example @stll/anonymize-darwin-arm64), resolved as optional dependencies at install time. Full package graph, runtime flow, and extension rules are in packages/anonymize/ARCHITECTURE.md.

Determinism and privacy

  • Deterministic output. The same input yields the same redaction on every run; detection is rule-driven with fixed priorities, not sampled.
  • Cross-runtime parity is tested. CI asserts the Node and Python SDKs return the same structured result from the same fixtures.
  • Data stays local. The CLI makes no network calls (stated in its --help); the SDKs load prepared packages from disk or bundled assets, and the browser build loads only the packages you bundle. No document text leaves the process.
  • No text in logs. Diagnostics and summaries carry entity counts, labels, spans, and scores, not detected text; raw input is kept out of logs and snapshots.

Versioning and status

Current release line: 2.0.0. In 2.0 the product runtime moved from the in-process TypeScript pipeline to the Rust-native SDK, and the package root now exports the native API (getDefaultNativePipeline whose pipelines expose redactText, plus redact_text, deanonymise, exportRedactionKey, and the prepared-package helpers). The 1.x TypeScript pipeline has been removed entirely; the Rust core owns detection, resolution, and configuration assembly across all runtimes. See packages/anonymize/ARCHITECTURE.md and packages/anonymize/CHANGELOG.md for the package-level history.

Packages

Package Purpose
@stll/anonymize Native runtime for multi-layer PII detection and anonymization
@stll/anonymize-wasm Browser/WASM build of the runtime
@stll/anonymize-cli Command-line anonymization (anonymize binary)
@stll/anonymize-data Published deny-list dictionaries and trigger/config data
stella-anonymize-core Python bindings for the Rust anonymization core
crates/anonymize-core Rust anonymization core

Development

bun install --frozen-lockfile
bun run lint
bun run typecheck
bun run test
bun run build

Git hooks (opt-in)

Lefthook config lives at lefthook.yml and is not auto-installed. To enable local hooks (format on pre-commit, typecheck + format check on pre-push):

bun run hooks:install
# bun run hooks:uninstall to remove

Release hygiene

  • Pinned GitHub Actions workflows validate lint, typecheck, tests, and package tarballs before release.
  • The data package tarball is checked so every exported dictionary path is present.
  • Release publishing is gated behind manual workflow dispatch and provenance-enabled npm publish steps.

Contributing

Contributions are welcome. Run bun run lint, bun run typecheck, and bun run test before opening a PR; a CLA check runs on pull requests. Please keep language data reproducible and out of source code, and do not commit raw personal data or non-public fixtures.

License

Apache-2.0. See LICENSE. Third-party runtime attributions for the browser build are listed in packages/anonymize/wasm/README.md.

About

Anonymization pipeline for sensitive text. Deterministic, local-first, fast.

Topics

Resources

License

Stars

3 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors