LFM2.5-Encoder-350M-PII-Detector β€” LiteRT

LiquidAI/LFM2.5-Encoder-350M-PII-Detector converted to LiteRT (.tflite) for on-device inference. Detects ~40 kinds of personal information across 16 languages, fully offline β€” a natural fit for on-device redaction where the text must never leave the phone (demo Space).

Model description

File Recipe Size Target
LFM2.5-Encoder-350M-PII-Detector_wi8fc.tflite int8 dynamic-range (linears + embedding, convs float) 364 MB mobile + desktop
LFM2.5-Encoder-350M-PII-Detector_fp16.tflite fp16 weights, float compute 712 MB desktop β€” full fidelity; phone memory limits (XNNPACK per-signature fp32 unpacking)

Two signatures, pii_128 and pii_512 (S = 128 / 512, batch 1, right-padded):

Tensor Shape Meaning
input_ids int32 [1, S] token ids (the tokenizer prepends <|startoftext|>)
attention_mask int32 [1, S] 1 = real token, 0 = pad
output_0 float32 [1, S, 161] BIOES logits, zeroed at padded positions

Take the argmax per token, then decode the BIOES spans. The logit axis is 161 wide but only ids 0–108 are defined β€” label_schema.json carries the 109-entry id2label map (id 0 = O, "not personal information"); the remaining slots are unused. Restricting the argmax to the first 109 columns is the safe reading.

How to use

1. Install dependencies

pip install ai-edge-litert numpy tokenizers huggingface_hub

2. Save the script below as detect_pii.py:

#!/usr/bin/env python3
"""Tag personal information with litert-community/LFM2.5-Encoder-350M-PII-Detector."""
import argparse
import json

import numpy as np
from ai_edge_litert.interpreter import Interpreter
from huggingface_hub import hf_hub_download
from tokenizers import Tokenizer

REPO = "litert-community/LFM2.5-Encoder-350M-PII-Detector"


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--text", required=True, help="Text to scan.")
    parser.add_argument("--seq-len", type=int, default=128, choices=[128, 512])
    args = parser.parse_args()

    model_path = hf_hub_download(REPO, "LFM2.5-Encoder-350M-PII-Detector_wi8fc.tflite")
    tokenizer = Tokenizer.from_file(hf_hub_download(REPO, "tokenizer.json"))
    schema = json.load(open(hf_hub_download(REPO, "label_schema.json")))
    id_to_label = {int(i): name for i, name in schema["id2label"].items()}

    encoding = tokenizer.encode(args.text)
    if len(encoding.ids) > args.seq_len:
        raise SystemExit(f"{len(encoding.ids)} tokens exceed --seq-len {args.seq_len}")

    input_ids = np.zeros((1, args.seq_len), np.int32)
    attention_mask = np.zeros((1, args.seq_len), np.int32)
    input_ids[0, : len(encoding.ids)] = encoding.ids
    attention_mask[0, : len(encoding.ids)] = 1

    interpreter = Interpreter(model_path=model_path)
    runner = interpreter.get_signature_runner(f"pii_{args.seq_len}")
    logits = runner(input_ids=input_ids, attention_mask=attention_mask)["output_0"]

    # Only ids 0..108 are defined in label_schema.json; the rest are unused slots.
    tags = logits[0, : len(encoding.ids), : schema["num_labels"]].argmax(-1)
    for token, tag in zip(encoding.tokens, tags):
        if int(tag) != 0:  # 0 = O, "not personal information"
            print(f"{token:20} {id_to_label[int(tag)]}")


if __name__ == "__main__":
    main()

3. Run it

python detect_pii.py --text "My email is jane@example.com and my phone number is 555-0142."
Δ jane                B-contact.email
@example             I-contact.email
.com                 E-contact.email
555                  B-contact.phone
-                    I-contact.phone
014                  I-contact.phone
2                    E-contact.phone

On Android/iOS use the LiteRT runtime's SignatureRunner APIs with the same signature names; the tokenizer is the standard Hugging Face tokenizer.json, which the Rust/Swift/Kotlin tokenizers bindings all read.

Performance

One pass over a padded sequence with the int8 (wi8fc) file, CPU only.

Device Threads pii_128 pii_512
Apple M4 Max (macOS) 8 34.9 ms 111.9 ms
iPhone 17 Pro 6 52 ms not measured

Mac figures are the median of 20 warm runs (ai-edge-litert 2.1.6, XNNPACK, otherwise idle machine). The iPhone figure comes from the on-device gate (TFLite C API + SignatureRunner + XNNPACK) and is a single run, not a median.

Budget for one slow first call. The first inference after loading pays a one-time graph preparation: on the Mac it took 376 ms against a 34.9 ms steady state. Later signatures on the same loaded model do not pay it again β€” pii_512 measured 110 ms cold against 112 ms warm. Model load itself was 0.38 s on the iPhone.

The signatures are fixed-shape, so the input language or content does not change the time β€” warm, pii_128 measures 36.4 / 37.3 / 36.6 ms on English, Japanese and Arabic sentences of 17, 21 and 27 tokens.

Accuracy note

Task-level parity against the PyTorch reference on a name + email + phone sentence: fp32 and fp16 reproduce the reference entity tags exactly. int8 keeps all multi-token spans (email, phone) intact and dropped exactly one tag in that test β€” an entity-end token whose fp32 decision margin was only 0.53 logits, a genuinely borderline call. That is the extent of what was checked; it is a single-sentence spot check, not a benchmark over a labelled corpus. If you need maximum recall on borderline tokens, use the fp16 file on desktop; on phones the int8 file is the artifact.

On the iPhone 17 Pro the int8 file reproduces the desktop outputs bit-exactly β€” cosine 1.000000, max absolute difference 0.0 over the full output tensor.

License

LFM Open License v1.0 (see LICENSE, unchanged from the base model). Note the license's commercial-use threshold (Section 5). This repository redistributes converted Derivative Works of LiquidAI/LFM2.5-Encoder-350M-PII-Detector with modification notices per Section 4; all credit for the model to Liquid AI.

Downloads last month
18
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support

Model tree for litert-community/LFM2.5-Encoder-350M-PII-Detector