Unquote-small
Strip the quoted history off an email without deleting the part you wrote.
Quoted replies, signatures and legal boilerplate are most of a mail thread and none of its meaning. Removing them before you pay a model to read the thread is easy money β right up until the cleaner eats a paragraph the sender actually wrote, and you have no way to notice.
unquote-small is a 31M-parameter CPU model that tags every line of an email
as one of eight zones. It is built around one asymmetry: leaving quoted text
in costs you tokens; taking body text out costs you the message. Every
decision is biased toward keeping.
Results
Measured on 1,200 held-out messages (synthetic + real public mailing-list mail). Splits are disjoint by thread, so no reply shares a split with the parent it quotes.
| System | body loss β | macro F1 β | token reduction | ms/msg |
|---|---|---|---|---|
| talon (rule-based standard) | 4.178% | 0.592 | 38.2% | 0.5 |
| email-reply-parser (rule-based) | 8.207% | 0.719 | 47.5% | 0.1 |
| Unquote (fp32, gated β shipped default) | 0.008% | 0.844 | 35.9% | 17.6 |
| Unquote (int8 ONNX) | 0.008% | 0.844 | 35.9% | 6.4 |
| Unquote (fp32, gate off) | 1.176% | 0.951 | 42.3% | 19.0 |
| Unquote heuristic (no weights) | 0.243% | 0.337 | 37.2% | 2.1 |
body loss is the fraction of true BODY lines a system deletes β the number
that decides whether a cleaner is safe to run unattended.
By language (synthetic split)
| System | en | fr | de | ja | ru | pt | ko | es | nl | it |
|---|---|---|---|---|---|---|---|---|---|---|
| talon (rule-based standard) | 0.510 | 0.568 | 0.459 | 0.480 | 0.467 | 0.458 | 0.459 | 0.387 | 0.523 | 0.299 |
| email-reply-parser (rule-based) | 0.700 | 0.756 | 0.600 | 0.751 | 0.632 | 0.747 | 0.612 | 0.669 | 0.662 | 0.677 |
| Unquote (fp32, gated β shipped default) | 0.823 | 0.873 | 0.874 | 0.906 | 0.914 | 0.899 | 0.866 | 0.846 | 0.873 | 0.879 |
| Unquote (int8 ONNX) | 0.825 | 0.873 | 0.879 | 0.905 | 0.914 | 0.899 | 0.866 | 0.846 | 0.874 | 0.878 |
| Unquote (fp32, gate off) | 0.955 | 0.973 | 0.969 | 0.997 | 0.984 | 0.973 | 0.948 | 0.964 | 0.975 | 0.989 |
| Unquote heuristic (no weights) | 0.229 | 0.226 | 0.266 | 0.411 | 0.252 | 0.265 | 0.194 | 0.255 | 0.256 | 0.230 |
Scored on the synthetic split because its labels are correct by construction. Non-English real mail in the evaluation corpus has pattern-derived gold for the trailer zones, which would be circular to score a rule-based baseline against.
Rule-based extractors carry English rule tables; this model has no such centre of gravity. English-vs-rest macro-F1 gap: talon +0.049, Unquote β0.059.
Use
from unquote import ZoneTagger
tagger = ZoneTagger.from_pretrained("NagaYu/unquote-small")
print(tagger.clean(raw_email)) # body only
pip install "unquote[export] @ git+https://github.com/NagaYu/unquote"
unquote clean mail.txt --keep body,attribution
Files in this repo: model.pt (torch), model.onnx (int8, 6.4 ms/msg),
model.fp32.onnx, tokenizer.json, config.json, gate.json (the fitted
safety thresholds), sweep.json (the full calibration curve).
What it promises
It will not delete your body text, bought in four places you can inspect:
| Mechanism | Strength |
|---|---|
BODY is not in the removable set; --keep quote still keeps body |
structural |
The gate only ever moves a label toward BODY |
structural, property-tested |
At gate threshold 1.0 nothing is removed at all |
mathematical |
| Per-zone thresholds fitted for zero body loss on held-out calibration data | empirical |
That last row is the honest one. A confidence gate cannot stop a confidently wrong prediction. On deliberately damaged mail at threshold 0.999 the measured body loss was mean 0.0005 / worst 0.0625, affecting 1 message in 120. The claim is "fitted to zero body loss and measured on held-out data", not an impossibility proof.
It does not promise a perfect split. Leftover quoted lines are an accepted outcome of that trade. If you need every quoted byte gone and can tolerate losing prose, this is the wrong tool.
Choosing an operating point
The gate ships fitted for zero body loss. That is one point on a measured curve:
| body-loss budget | threshold | measured body loss | token reduction |
|---|---|---|---|
| 0.00% | 0.9999 |
0.000% | 34.7% |
| 0.05% | 0.99 |
0.045% | 52.0% |
| 0.20% | 0.915 |
0.193% | 53.4% |
| 0.50% | 0.705 |
0.499% | 54.2% |
| 1.00% | 0 |
0.728% | 54.7% |
The curve is steep β accepting roughly one lost line in 2,200 nearly doubles the
savings. Move along it with --threshold, or refit with
scripts/train.py --target-body-loss 0.002.
Cost of the strict end: at zero body loss the gate rescues signatures the model
was 89% sure about, so SIGNATURE F1 falls from 0.897 ungated to 0.429 gated.
That is the trade working as designed, but it leaves more signature text in
place than the model is capable of removing.
The eight zones
BODY (never removable), QUOTE, ATTRIBUTION, SIGNATURE, DISCLAIMER,
MOBILE_FOOTER, AUTOREPLY, FORWARD_HEADER.
Two conventions: everything inside a quoted message collapses to QUOTE
(including its signature β a quoted signature is not this author's), and the
introducer line keeps its own zone at every nesting depth.
Architecture
tokens of one line -> embedding -> shift-mix -> masked mean+max pool
concat 88 structural features (quote depth, script, wrap width, distance from end)
-> transformer over the LINE sequence <- where the evidence actually is
-> per-line emissions + learned transitions
-> Viterbi over (zone, seen_trailer) <- "no body after a signature", enforced
8 document layers, d_model 512, vocab 21,079, 31.1M parameters. Position is encoded from both ends: trailer zones are anchored to the end of a message, so the encoder gets distance-from-end alongside distance-from-start.
Decoding runs Viterbi over the augmented state (zone, seen_trailer), which
latches once a signature/disclaimer/mobile-footer line is emitted; BODY is
-inf in the latched half. No body line can follow a signature no matter what
the network scores β including via the SIGNATURE -> QUOTE -> BODY path that a
pairwise transition ban cannot block.
Training data
Public mailing-list archives only β Apache (lists.apache.org), Python
(mail.python.org), GNU (lists.gnu.org) and the localized Debian lists in 15
languages β plus synthetic threads from unquote.compose whose labels are
correct by construction.
Excluded on purpose: Enron and every corpus like it. Freely downloadable, and still a collection of private messages whose authors never consented to publication. Public availability is not consent. The dataset builder enforces a deny-list.
Privacy. Every real message is scrubbed before training: addresses become
RFC 2606 reserved domains, phone numbers move to the +1-555-01xx fictional
range, and every participant's name is replaced β not just the sender's, since
attribution lines and quoted signatures name third parties. Archive-obfuscated
addresses (a <at> b.com) are handled too. Replacements are
structure-preserving, so the tagging task is unchanged.
Limitations
- Throughput is tens to hundreds of messages/second, not thousands. 17.6 ms/msg fp32, 6.4 ms/msg int8, single process on 6 CPU threads. A 31M transformer over a line sequence does not reach thousands/sec on one core.
- Trained on developer mailing lists. Corporate mail, marketing mail and ticketing-system notifications are out of distribution.
- Real-data gold for
SIGNATURE/DISCLAIMERis pattern-derived; those rows are flagged in the benchmark output and are not admissible evidence against a rule-based baseline. - HTML-only messages are dropped at ingest β without line structure there is nothing to line-tag.
- An earlier checkpoint trained on synthetic data alone scored 0.982 on synthetic and 0.321 on real mail. Withholding real data did not make the evaluation honest, it made the model useless; thread-disjoint splitting is what protects it instead.
Citation and source
Code, benchmark harness, dataset builder and the four guarantee tests: https://github.com/NagaYu/unquote
Licensed Apache-2.0. Archive content remains under the terms of the archives it came from.
- Downloads last month
- 56
Evaluation results
- macro F1 (line-level, gated) on Unquote zone-annotation corpus (held-out test split)test set self-reported0.844
- macro F1 (line-level, ungated) on Unquote zone-annotation corpus (held-out test split)test set self-reported0.951
- body-loss rate (fraction of true BODY lines deleted) on Unquote zone-annotation corpus (held-out test split)test set self-reported0.000