File size: 8,775 Bytes
7b74a68 24819f9 387e2ff 5304001 387e2ff 7b74a68 757ed9b 7b74a68 5304001 7b74a68 387e2ff 24819f9 7b74a68 387e2ff 7b74a68 5304001 ac20c52 5304001 ac20c52 5304001 7b74a68 24819f9 7b74a68 ac20c52 5304001 ac20c52 7b74a68 ac20c52 7b74a68 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | ---
license: mit
language:
- en
library_name: cite
tags:
- cite
- paragraph-citation
- retrieval
- question-answering
- news
- encoder
- modernbert
- on-device
pipeline_tag: text-ranking
base_model: jhu-clsp/ettin-encoder-32m
---
# Cite
Which paragraphs answer the question.
A small encoder that, given a question and ordered paragraph texts, returns a
calibrated probability per paragraph and two thresholds (`needed`,
`contributing`).
| | |
| --- | --- |
| **Task** | Paragraph citation / needed-blocks |
| **Size** | ~42 MB weights (trimmed vocab, int8 embeddings, fp16 rest); a larger 68M build is under [`68m/`](https://huggingface.co/localangle/cite/tree/main/68m) |
| **Runtime** | CPU (PyTorch); browser (ONNX Runtime WASM) |
| **Weights** | [v0.3.0](https://huggingface.co/localangle/cite/tree/v0.3.0) · [`web/`](https://huggingface.co/localangle/cite/tree/main/web) for WASM |
| **Demo** | [localangle/cite-demo](https://huggingface.co/spaces/localangle/cite-demo) (in-browser) |
Cite does **not** write an answer. It ranks the paragraphs you already have and
flags which ones are needed to answer the question.
> Question: *Who voted against the library renovation?*
> → Cite highlights the paragraph naming Paulsen and Chen.
## Try it
- **Live demo:** [localangle/cite-demo](https://huggingface.co/spaces/localangle/cite-demo) — paste an article, ask a question, see which paragraphs Cite cites.
- **Local playground:** from the [cite](https://github.com/localangle/cite) repo, `uv run python deploy/cite_playground.py`.
## Install
```bash
pip install "cite @ git+https://github.com/localangle/cite.git"
# or, from a checkout:
uv sync
```
Python ≥ 3.12. Needs `torch`, `transformers`, `safetensors`, `huggingface_hub`.
## Usage
```python
from cite import CitePackage
cite = CitePackage.from_pretrained("localangle/cite")
paragraphs = [
"The council approved a $4.2 million library renovation.",
"Paulsen and Chen voted against the measure.",
"Construction starts in March.",
]
scores = cite.predict("Who voted against the renovation?", paragraphs)
for score in scores:
if score.needed:
print(score.index, score.probability, paragraphs[score.index])
```
`score.needed` uses the packaged threshold (0.76). `score.contributing` is a
looser relatedness cut (0.17). Logits are Platt-scaled to probabilities at load
time; you do not fit thresholds yourself for the published package.
Load a local folder the same way:
```python
cite = CitePackage.load("path/to/package") # or from_pretrained("./path")
```
## Files
| File | Contents |
| --- | --- |
| `cite.json` | Format, Platt scaling, thresholds, source metadata |
| `weights.safetensors` | Trimmed encoder + head (int8 token embeddings, fp16 elsewhere) |
| `encoder/` | ModernBERT/Ettin config for the trimmed vocabulary |
| `tokenizer/` | Tokenizer with `[MASK]` as the block marker |
| `web/` | Browser package: `model.onnx` (~36 MB int8), `vocab_map.bin`, `cite-web.json`, `tokenizer/` |
| `68m/` | The 68M build as a full Python package (same layout as the repo root; ~107 MB weights) |
| `web-68m/` | Browser package for the 68M build (`model.onnx` ~88 MB int8) |
PyTorch weights expand to fp32 at load; only storage is compressed. The `web/`
ONNX graph is what the static demo runs in WASM (soft cap 2048 tokens).
## Accuracy
On a held-out real-news needed-blocks eval (v59, 1,800 questions over local
news outlets; threshold chosen on a separate calibration split):
| Metric | Cite-32M package |
| --- | --- |
| Coverage | 0.692 |
| Exact set match | 0.725 |
| Empty-set accuracy | 0.897 |
| Any-extra rate | 0.066 |
The browser ONNX build on the same eval: exact 0.722, coverage 0.687, empty 0.902.
### Broad and abstract questions
The eval above uses specific fact-lookup questions. Later versions add training so that broad
questions ("How will the library change as a result of the renovations?") score the paragraph
that answers them instead of washing out.
- **v0.2.0** fine-tuned on LLM-written broad rewrites of real training questions.
- **v0.3.0** adds synthetic short articles where nearly every paragraph shares the topic word,
with hard negatives, then averages the weights 0.75/0.25 with v0.2.0 to keep the specific-question
operating point.
On a 117-item hand-written probe (specific question vs. an abstract rewrite of it; 92 items after
removing 25 where several paragraphs plausibly answer the rewrite), abstract-question results:
| Abstract question | v0.1.0 | v0.2.0 | v0.3.0 |
| --- | --- | --- | --- |
| Top-1 (all 117) | 0.650 | 0.744 | 0.752 |
| Needed recall (all 117) | 0.402 | 0.752 | 0.761 |
| Top-1 (92 audited) | 0.826 | 0.848 | 0.859 |
| Top-3 (92 audited) | n/a | 0.957 | 0.978 |
Packaged-model numbers for v0.2.0 and v0.3.0; v0.1.0 from the fp32 student. Specific-question probe
top-1 is 0.949 (v0.2.0) and 0.966 (v0.3.0). The probe is small and written by one author;
treat it as a regression check, not a benchmark.
On a short, topic-saturated example article, v0.3.0 improves but does not solve the hardest
case: for "What specific improvements will be made...?" the improvements paragraph moves from
rank 4 (18%) to rank 2 (60%), still behind a funding paragraph that repeats the word
"renovation". See `experiments/107-cite-abstract-probe`, `experiments/108-cite-broad-consistency`
and `experiments/109-cite-hard-negatives`.
## Cite 68M
A larger build, distilled from the same teacher with the same data and training steps, on
[Ettin-68M](https://huggingface.co/jhu-clsp/ettin-encoder-68m). It is more accurate, especially on broad
and abstract questions, and about 3x slower. The 32M build above stays the default.
```python
cite = CitePackage.from_pretrained("localangle/cite", subfolder="68m")
```
The [demo](https://huggingface.co/spaces/localangle/cite-demo) has a model switch; the 68M build downloads
about 88 MB on first use and is cached after that. Packaged thresholds: needed 0.79, contributing 0.17.
| | Cite 32M (v0.3.0) | Cite 68M |
| --- | --- | --- |
| Weights (package) | 41.8 MB | 107.0 MB |
| Browser `model.onnx` | 36.3 MB | 88.3 MB |
| v59 eval: coverage / exact / empty-set / any-extra rate | 0.692 / 0.725 / 0.897 / 0.066 | 0.729 / 0.763 / 0.923 / 0.042 |
| Abstract probe, 92 audited items: top-1 / needed recall | 0.859 / 0.859 | 0.902 / 0.891 |
| Abstract probe, all 117: top-1 | 0.752 | 0.786 |
| Browser (WASM, 1 thread): 7 paragraphs / 44 paragraphs | 185 ms / 1.4 s | 547 ms / 4.2 s |
On the short example article, for "What specific improvements will be made to the library as a result of the
renovation?", the improvements paragraph scores 93.5% on 68M (60% on 32M, behind a funding paragraph).
The same caveats apply: one run per size, a small hand-written probe, and browser timings from a single
machine. See `experiments/110-cite-68m`.
## Latency
Batch-1, CPU, AMD EPYC (HF `cpu-upgrade`, 8 vCPU) for the related ONNX int8
form: about 44 ms p50 at 5–12 paragraphs and 268 ms at 33–60. The portable
PyTorch package this repo ships is the accuracy-matched serving artifact; long
documents are dominated by attention cost.
## How it works
Cite is a fine-tuned [Ettin](https://huggingface.co/jhu-clsp/ettin-encoder-32m)
encoder (ModernBERT family). The question and each paragraph are packed into
one sequence with a `[MASK]` marker before every paragraph; the head reads the
marker states. A 400M teacher labeled real news; the 32M student was distilled
then fine-tuned on LLM-written broad rewrites of its training questions
(consistency distillation) and on synthetic hard-negative articles, and compressed (trimmed vocabulary, int8
embeddings, fp16 weights).
## Limitations
- Cite ranks paragraphs; it does not generate answers or quote spans inside a
paragraph.
- Lexical overlap still matters: "big 10" and "Big Ten" can score differently.
Scores for a paragraph also drift down in very long articles.
- Eval is English news-style text. Other domains are unmeasured.
- Empty-set accuracy drops a few points from calibration to eval; treat the
packaged threshold as a starting point for a new domain.
- Documents longer than `max_tokens` (7999) drop trailing paragraphs.
- Training used private teacher labels and news text that is not redistributed
here.
## Built on
- [Ettin encoder 32M](https://huggingface.co/jhu-clsp/ettin-encoder-32m) (MIT)
by JHU CLSP. Fine-tuned and compressed for Cite; see
`THIRD_PARTY_NOTICES.md`.
## License
MIT for the Cite code and this package's weights, subject to the Ettin MIT
notice above.
## Citation
```bibtex
@software{cite_2026,
title = {Cite: Which paragraphs answer the question},
author = {Local Angle},
year = {2026},
url = {https://huggingface.co/localangle/cite},
}
```
|