Mystery_Mail_Guardian / docs /hf-article.md
Auenchanters's picture
docs: add published Field Notes article source (HF blog)
968e7d8
|
Raw
History Blame Contribute Delete
9.77 kB
# Building Mystery-Mail Guardian: A Project for build-small-hackathon
## Introduction
My grandmother can read English. What she can't always do is tell a *real* letter
from a scam — the official-looking envelope demanding payment "within 24 hours,"
the one threatening arrest, the one that's secretly an advertisement dressed up
as a bill. For a lot of elderly people, the mailbox is one of the last places
they still get attacked, and the attacks are getting better.
The obvious move is to point a multimodal model at the letter and ask "is this a
scam?" But the letters that matter most — from the bank, the tax office, the
doctor — are exactly the documents you should *never* upload to a cloud API. So
the interesting constraint wasn't "can a model read a letter." It was: **can a
model read a scary letter, helpfully and safely, without a single byte ever
leaving the device?**
That constraint became the whole project.
## The Idea
**Mystery-Mail Guardian** lets you photograph a confusing letter and get back, in
plain words: what it is, whether you should worry (with concrete reasons, never a
verdict), what to actually do next — and it reads the answer aloud. In English,
Hindi, Spanish, and Japanese.
Two design rules shaped everything:
1. **Run entirely on-device.** No cloud inference at runtime. The privacy promise
isn't a feature bolted on; it's the reason the product gets to exist.
2. **Never let the model have the final word on safety.** A small vision model
*will* occasionally say something confidently wrong. So the model proposes,
and a separate, deterministic safety layer disposes.
## High-Level Plan: small models, watched closely
Everything runs on two OpenBMB models, **3.3B parameters total** — comfortably
under the hackathon's cap, and small enough to run on a single ZeroGPU slice:
- **MiniCPM-V 4.6** (1.3B) is the central engine: it reads the photographed
letter (OCR + layout + reasoning) and returns structured JSON.
- **VoxCPM2** (2B) reads the result aloud, in the user's language, with a neutral
built-in voice (deliberately no voice cloning — no consent question to answer).
There was a tempting third option: bolt an 8B reasoner on top for nicer prose.
I built the switch for it, measured it, and left it off — more on that below. The
hackathon's "Tiny Titan" award is for models ≤4B, and the lean pair already does
the job. Smaller was the right call, not the compromise.
## Internal Logic: the model proposes, the safety layer disposes
The pipeline is four stages — **extract → triage → safety → speak** — and the
single most important architectural decision is that the safety rules live in
*code*, in their own module, not in the prompt. A prompt can be ignored by a
0.8B language head having a bad day. A regex cannot.
### Stage 1 — Extract
MiniCPM-V 4.6 reads the photo against a strict JSON schema. The prompt bakes in
the safety rules too (defense in depth), but its real job is to pull *structured
facts* — sender, amount, deadline, and any scam signals it spots from a fixed
taxonomy:
```python
"scam_signals": [
{"id": "<signal id>", "evidence": "short quote or fact from the document"}
],
"explanation": {
"what_this_is": "1-2 short sentences in {language}, words a 10-year-old understands",
"key_facts": ["up to 4 very short facts with real values from the letter"],
"worry_reasons": ["each scam signal explained in one short sentence"],
"what_to_do": ["2 to 4 short, safe, concrete steps"]
}
```
### Stage 2 — Triage (a second pair of eyes)
Here's the part I trust most, because it doesn't trust the model. An *independent*
heuristic scan runs over everything the model extracted, looking for scam
vocabulary the model might have downplayed — gift cards, wire transfers, OTP
requests, arrest threats, urgency — in every supported script:
```python
"gift_card_or_crypto": re.compile(
r"gift\s*card|itunes\s*card|bitcoin|crypto|गिफ्ट\s*कार्ड|"
r"ギフトカード|tarjeta\s+de\s+regalo", re.I),
"threat_or_arrest": re.compile(
r"\barrest(?:ed)?\b|lawsuit|गिरफ़्तार|逮捕|arresto", re.I),
```
The model's signals and the heuristic signals are unioned, and the worry level
is computed from the result — never from the model's mood:
```python
def worry_level(document_type, signals):
if document_type == "suspected_scam" or len(signals) >= 2:
return "warning"
if len(signals) == 1:
return "caution"
return "low" # note: there is no "safe" — only "looks normal, but verify"
```
There is deliberately **no "safe" state**. The best case is "this looks like
normal mail, but always double-check before paying or replying."
### Stage 3 — The safety layer
Every piece of user-facing text passes through here. Two guarantees matter most.
**It softens verdicts.** If the model writes "this is definitely a scam," it
becomes "this looks like it could be a scam." We never tell a 78-year-old that a
real letter is definitely safe, or that a real bill is definitely fraud.
**It strips the letter's own contact details — from every card.** A scam letter's
entire goal is to get you to call *its* number. So phone numbers, links, and
emails printed in the letter are removed from the answer, and replaced with one
constant instruction: contact the company through a number *you already trust*.
```python
_PHONE_RE = re.compile(r"\+?\d[\d\-\s().]{7,}\d")
_EMAIL_RE = re.compile(r"\b[\w.+-]+@[\w-]+\.[\w.]+\b")
def strip_contact_details(text: str) -> str:
text = _URL_RE.sub("", text)
text = _EMAIL_RE.sub("", text)
text = _PHONE_RE.sub("", text)
return re.sub(r"[ \t]{2,}", " ", text).strip(" \t,;:")
```
This earned its keep in live testing. Against a real gift-card scam, the model
suggested *"check with the tax bureau and ensure you pay soon"* — genuinely
dangerous advice. Because it arrived in the wrong shape, the composer discarded
it and showed the safe fallback plus the verification advice instead. The
scammer's 1-800 number appeared nowhere on the screen.
### Stage 4 — Speak
VoxCPM2 turns the safety-checked summary into speech on demand — one big "Read it
aloud" button that only appears once there's something to read.
## The QA flywheel: Modal as the proving ground
Here's a thing that surprised me: the most valuable use of GPU credits wasn't
inference, it was *testing*. The deployed Space runs on a daily ZeroGPU quota I
didn't want to burn on my own QA — so every bit of GPU validation runs on
**Modal** instead, and the deployed runtime stays 100% local. Off-the-grid stays
intact; QA gets unlimited room.
I built a little factory that forges synthetic letters with *gold labels derived
through the app's own triage rules* (so the labels can never drift from the
product), renders them as photos, degrades them like a shaky phone hand, and
scores the full pipeline on an A10G. It paid for itself immediately:
- **Run #1 of the validation matrix** caught a real bug — a lottery scammer's
reply address leaking through the "key facts" card. (The contact-stripping
only covered action steps; now it covers every card.)
- **A multilingual eval** found Hindi and Japanese scam letters being
under-flagged, which led to original-script quoting in the prompt and
multilingual scam vocabulary in the heuristics.
- **A robustness sweep** — six degradation types × three intensities (blur to
4.5px, brightness to 15%, rotation to 35°, perspective warp, hard shadow,
sensor noise) — came back **38/38 honest outcomes**. The scam stayed *warning*
and the bill stayed *low* through all of it.
And the answer to "should we fine-tune?" came from the data, not from wanting to.
I trained a LoRA adapter anyway, to prove the loop works — train loss 2.14 → 0.03
in under six minutes for about forty cents — but the evals showed safety behavior
was already at ceiling on the stock model, so the adapter stays shelved through
judging. With small models, you earn trust through guardrails and measurement,
not through more training.
## A note on the front end
The UI is hand-built past the default Gradio look into a warm "kitchen-table post
office": letters render as paper sheets, the verdict is a perforated postage
stamp, the privacy promise is a green wax seal. The body font is **Atkinson
Hyperlegible**, designed by the Braille Institute specifically for low-vision
readers — an elder-readability font in an elder-readability app — vendored into
the repo so even the fonts make no network request. Every color pair is enforced
against WCAG AA contrast by unit tests; if a pretty color fails, the color
changes, not the test. (There may also be a Gen X Soft Club easter egg hiding
behind one of the language buttons. I'll let you find it.)
## Wrapping up
What I like most isn't any single trick — it's the shape of the thing. A model
small enough to run in your hand, wrapped in code that refuses to let it be
confidently wrong about something that could cost someone their savings. The
intelligence proposes; the rules protect.
The honest catch is **script coverage**: a 1.3B vision model reads English (and
Japanese) print reliably, but Devanagari is near its limit, so Hindi letters can
be under-read — which is exactly why every single result, in every language,
still ends with "check with someone you trust." The guardrails hold even where
the model doesn't.
It's live, free, and fully open. Built for the Hugging Face **Build Small**
hackathon. If it speaks to you — or to someone you'd want to protect — give it a
try, and a ❤️ on the Space genuinely helps. 🤗
👉 **https://huggingface.co/spaces/build-small-hackathon/Mystery_Mail_Guardian**