File size: 7,192 Bytes
c82958b
 
 
 
 
 
 
 
 
4f373f2
 
 
c82958b
4f373f2
 
 
c82958b
4f373f2
 
c82958b
 
 
 
 
 
 
 
 
 
 
 
 
1e7143f
 
c82958b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1e7143f
c82958b
 
 
 
 
 
 
 
 
 
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
---
license: cc-by-nc-4.0
language:
- en
- vi
base_model: google/gemma-4-E4B-it
pipeline_tag: token-classification
library_name: transformers
tags:
- systemone
- extraction-model
- text-to-typed-output
- information-extraction
- key-information-extraction
- extractive-qa
- span-extraction
- structured-output
- document-ai
- multilingual
- gemma4
---

# gemmaExtract

**Pull typed fields out of a document in one forward pass. No text generation, no broken JSON.**

You give gemmaExtract a document and a list of fields you want. It returns each field's value, where that value sits in the text, whether the field is absent, and how confident it is. Values are read straight from the document (or computed from numbers in it), so the model cannot invent a string that is not there.

```
document  ──►  one forward pass  ──►  {field: value, span, p_null, confidence}
```

> **Text input only.** gemmaExtract currently reads plain text (`state` must be a string). For scanned documents, photos or PDFs, run OCR first and pass the recognized text. Image input is not supported yet.

## Quick start

```python
import sys
from huggingface_hub import snapshot_download

path = snapshot_download("beyoru/gemmaExtract")
sys.path.insert(0, path)
from gemma_extract import GemmaExtract

model = GemmaExtract.from_pretrained(path)   # needs a CUDA GPU, ~18 GB in bf16

doc = """INVOICE No: INV-2026-0042      Date: 15/01/2026
Seller: Northwind Office Supply Ltd.
1. A4 paper (box)      x10   45.00   450.00
2. Toner cartridge     x2    120.00  240.00
Subtotal: 690.00   Tax 10%: 69.00   TOTAL DUE: 759.00
Paid: 800.00"""

out = model.extract(doc, {
    "invoice_no": {"type": "text",   "instructions": "Invoice number"},
    "date":       {"type": "date",   "instructions": "Invoice date"},
    "items":      {"type": "list",   "instructions": "Names of the items bought"},
    "total":      {"type": "number", "instructions": "Total amount due"},
    "po_number":  {"type": "text",   "instructions": "Purchase order number"},
})
```

```json
{
  "invoice_no": {"value": "INV-2026-0042", "spans": [{"start": 12, "end": 25, "text": "INV-2026-0042"}], "null": false, "p_null": 0.0001},
  "date":       {"value": "2026-01-15", "guess": "15/01/2026", "null": false},
  "items":      {"value": ["A4 paper (box)", "Toner cartridge"], "null": false},
  "total":      {"value": 759.0, "guess": "759.00", "op": "none", "null": false},
  "po_number":  {"value": null, "null": true, "p_null": 1.0}
}
```

Vietnamese documents work the same way:

```python
model.extract("HÓA ĐƠN GIÁ TRỊ GIA TĂNG ... Ngày 20 tháng 10 năm 2026 ... Tổng cộng thanh toán: 3.685.000",
              {"ngay": {"type": "date", "instructions": "Ngày lập hoá đơn"},
               "tong": {"type": "number", "instructions": "Tổng cộng thanh toán"}})
# {"ngay": {"value": "2026-10-20"}, "tong": {"value": 3685000.0}}
```

## Field types

| type | returns | notes |
|---|---|---|
| `text` | string + character span | copied verbatim from the document |
| `list` | list of strings + spans | every item that matches |
| `number` | float | read from the document, or computed from numbers in it (`sum`, `diff`, `avg`, `count`, `div`, `mul`; operands are returned) |
| `date` | `YYYY-MM-DD` | day-first by default, Vietnamese and English formats |

Every field also gets `p_null` (probability the field is absent), `null`, `confidence` and `guess` (the best candidate, returned even when the field is judged absent).

Options:
- `null_threshold`: per request or per field. The default depends on the detected language (`en` 0.40, `vi` 0.80, any other language 0.80) and leans conservative: it rarely reports a present value as missing. Raise recall of absent fields by lowering it.
- `number_style`: `"en"` (1,234.5) or `"vi"` (1.234,5). By default it is detected from the document.

## Results

Measured on half of an H200, bf16, batch 1, default null threshold.

**Reading comprehension with unanswerable questions** (1,000 held-out questions each, natural mix of answerable and unanswerable):

| | accuracy | absent field reported as present | present field reported as absent | F1 on answerable |
|---|---|---|---|---|
| SQuAD 2.0 validation (EN) | 79.3% | 147 / 495 | 9 / 505 | 0.93 |
| UIT-ViQuAD 2.0 validation (VI) | 81.4% | 63 / 318 | 16 / 682 | 0.91 |

**Other languages** (XQuAD, 300 questions per language, all answerable). The extraction head was not tuned for these languages; they come from the multilingual Gemma 4 backbone:

| language | EM | F1 | present field reported as absent |
|---|---|---|---|
| English | 70.3 | 86.6 | 1 / 300 |
| German | 60.3 | 80.4 | 2 / 300 |
| Spanish | 58.0 | 80.2 | 6 / 300 |
| Chinese | 64.7 | 84.1 | 4 / 300 |
| Russian | 61.3 | 79.5 | 7 / 300 |
| Thai | 63.3 | 84.1 | 5 / 300 |
| Arabic | 56.7 | 77.3 | 9 / 300 |
| Hindi | 59.3 | 77.0 | 2 / 300 |

(Absent-field counts use the threshold applied to each language: 0.40 for English, 0.80 otherwise.)

**Receipt fields** (SROIE test, 361 receipts, exact match):

| company | date | total | address (F1) |
|---|---|---|---|
| 96.4 | 98.9 | 93.4 | 67.5 |

**Speed**: the document is read once for all fields, so latency barely moves with the number of fields. Model pass for 1 / 5 / 10 fields: 52 / 53 / 54 ms (16 fields per pass; 20 fields take two passes, ~110 ms). A full `extract()` call on a 5-7 field invoice, including decoding and a small HTTP wrapper, takes ~150-180 ms.

## Limitations

- **Arithmetic over the document** (e.g. change = paid − total) is partial: about 7 in 10 computed answers are right on English financial tables and about 1 in 2 on English narrative text; on Vietnamese documents it is not reliable yet and may return one of the operands instead. Copy-type numbers (totals, amounts printed on the document) are much more reliable.
- **Span-based**: answers that must be paraphrased or merged from scattered places (long free-form answers, multi-line addresses) are weaker than with a generative model.
- Up to **2,048 tokens per pass**; longer documents are truncated (`usage.truncated` tells you).
- Languages other than English and Vietnamese work (see the XQuAD table) but absent-field detection has only been checked in English and Vietnamese.
- Text input only: images, scans and PDFs need an OCR step first.
- Not supported yet: `time`, `duration`, `estimate`, `enum` fields and image input.
- Runs with `transformers` (≥ 5.17); there is no serving-engine integration yet.

## How it works, briefly

A Gemma 4 E4B backbone whose global-attention layers attend in both directions, plus a small extraction head. The document comes first and the field schema after it; each field's description becomes a query that points to the start and end of its value in the document, decides whether the field is absent, and, for numbers, picks an operation and its operands. Nothing is decoded token by token.

## License

CC BY-NC 4.0: free for non-commercial use with attribution; commercial use needs prior written permission from the author ([beyoru](https://huggingface.co/beyoru)). gemmaExtract is derived from Gemma 4 E4B (Apache 2.0); see `NOTICE` and `LICENSE-APACHE-2.0`.