Granite 4.0 1B invoice-to-JSON LoRA adapter (MLX)
A LoRA adapter for ibm-granite/granite-4.0-1b that reads the extracted text of an invoice or credit note and returns its 8 header fields as one JSON object: vendor_name, invoice_number, invoice_date, currency, subtotal, tax, total, document_type. Trained and run with Apple MLX (mlx-lm).
Training status: completed.
Model overview
| Task | Invoice text → JSON with 8 header fields |
| Base model | ibm-granite/granite-4.0-1b (revision 6a7381ba1f54d684ff508d991aeb7dc580157103) |
| Pre-trained from | ibm-granite/granite-4.0-1b-base |
| Architecture | Granite 4.0 (granitemoehybrid; this size uses 40 dense attention blocks, no experts) |
| Fine-tuning method | LORA on every linear layer of every block |
| Framework | MLX / MLX-LM (Apple Silicon, Metal) |
| Intended inference environment | macOS on Apple Silicon with mlx-lm |
| Project status | experimental / educational; training status: completed |
Model details
- Base model:
ibm-granite/granite-4.0-1bat commit6a7381ba1f54d684ff508d991aeb7dc580157103, bf16 safetensors, licenseapache-2.0. - This repository contains only a LoRA adapter (
adapters.safetensors+adapter_config.json). It is not a standalone model and must be loaded on top of the exact base model above. - Weight format: safetensors (MLX). The adapter was trained against the bf16 base; no quantization.
- Parameters: trainable 20.316M of 1631.75M total (as reported by MLX-LM).
- Software: mlx 0.32.4, mlx-lm 0.32.2, pypdf 6.21.3.
Intended use
Turning the text layer of a digital invoice or credit note (for example from pypdf) into structured header data for experiments, teaching, and as a first pass in an accounts-payable workflow that a person reviews.
Every extracted value must be checked before it is used for payments, bookkeeping or tax filings. Out of scope: line items, scanned or photographed documents (no OCR), statements and multi-invoice files, and any fully automated payment decision.
Training data
- Source: 2,000 synthetic documents generated by this project (seed 1); no real or benchmark document was used.
- Coverage: 11 locales (US, GB, DE, FR, NL, ZA, NA, ZM, IN, AU, CA) with their currencies, tax labels and rates, date and number formats; about 12% credit notes (minus, brackets,
CRor no sign), about 15% tax-inclusive layouts, fees and discounts, 1–30 line items, multi-page tables with carried/brought-forward rows, and distracting numbers (order numbers, due dates, account and bank details). - Prepared composition: credit_note 232, invoice 1768; currencies AUD 165, CAD 203, EUR 572, GBP 170, INR 183, NAD 185, USD 184, ZAR 169, ZMW 169.
- Overlap with the test set (normalised vendor, invoice number, identical text): vendor_name 0, invoice_number 0, identical_text 0.
- Prompt (user turn):
Extract this invoice as JSON with exactly these keys: vendor_name, invoice_number, invoice_date (YYYY-MM-DD), currency (ISO code), subtotal, tax, total (numbers), document_type (invoice or credit_note).
{invoice text}
- Target (assistant turn): the 8-field answer as one line of JSON (
json.dumps, keys in the order above). Amounts are positive numbers; the credit decision isdocument_type. - Format: MLX-LM chat JSONL; loss on the assistant answer only (
mask_prompt). There is no validation split. - Limitations: one generator means limited layout variety; mostly English with Southern African, UK, US, EU, Indian and Australian/Canadian conventions.
Training procedure
| Setting | Value |
|---|---|
| Profile | m1_air_8gb |
| Method | lora (rank 16, scale 2.0, dropout 0.0) |
| Target layers | every linear layer (attention q/k/v/o and MLP) of every block |
| Optimizer | adamw, weight decay 0.0, bias correction on |
| Learning rate | 0.0002, cosine schedule, 20 warmup updates |
| Iterations | 1200 micro-batches of 1, gradient accumulation 4 |
| Max sequence length | 1536 |
| Prompt masking / gradient checkpointing | True / True |
| Seed | 42 |
| Final train loss | 0.0 |
| Wall time | 30.0 s |
| Peak memory footprint | 6.82 GB |
| Hardware | Apple M1, 8 GB unified memory |
| OS / Python | macOS 27.0 / Python 3.12.15 |
| Run id | m1_air_8gb-20261028-150545 |
Evaluation
Test set: the 100 digital PDFs of drew-ipp/invoice-extraction-benchmark (revision 446bab08662790cb39457792243bff5704373b95) in the categories clean, currency, tax, credit, multipage, freight; photos, scans, handwriting and the benchmark's "hard" category are excluded. Text is extracted with pypdf. None of these documents was used for training. Greedy decoding, at most 160 new tokens; the JSON is the span from the first { to the last } of the reply.
Field rules (header fields of the benchmark's published rules, without its fuzzy vendor matching): amounts exact to the cent, comparing magnitudes; vendor names equal after lower-casing and dropping punctuation and legal forms; invoice numbers equal after removing case, spaces, #, : and a leading label; date, currency and document type equal ignoring case. A document counts only when all 8 fields are right.
| Model | All 8 fields right | Fields right | Valid JSON | Date |
|---|---|---|---|---|
Base ibm-granite/granite-4.0-1b |
54 / 100 (54.0%) | 720 / 800 (90.0%) | 100.0% | 2026-10-28 |
| Base + this adapter | 83 / 100 (83.0%) | 775 / 800 (96.9%) | 100.0% | 2026-10-28 |
100 synthetic benchmark documents from one layout engine are a small and narrow test: a 95% interval on the document score is up to about ±10 percentage points, and real supplier invoices are more varied.
How to use
Requires macOS on Apple Silicon.
pip install "mlx-lm>=0.32.2,<0.33" huggingface_hub pypdf
import pypdf
from huggingface_hub import snapshot_download
from mlx_lm import generate, load
adapter_dir = snapshot_download("thealper2/mlx-granite-4.0-1b-invoice-to-json")
model, tokenizer = load(
"ibm-granite/granite-4.0-1b",
revision="6a7381ba1f54d684ff508d991aeb7dc580157103",
adapter_path=adapter_dir,
)
text = "\n".join(page.extract_text() for page in pypdf.PdfReader("invoice.pdf").pages)
prompt = (
"Extract this invoice as JSON with exactly these keys: vendor_name, invoice_number, invoice_date (YYYY-MM-DD), currency (ISO code), subtotal, tax, total (numbers), document_type (invoice or credit_note).\n\n"
+ text
)
chat = tokenizer.apply_chat_template(
[{"role": "user", "content": prompt}], add_generation_prompt=True, tokenize=False
)
print(generate(model, tokenizer, prompt=chat, max_tokens=160))
Use exactly this instruction and the model's own chat template; the adapter was trained on it. mlx_lm.load applies the adapter from a local folder, which is why the repository is downloaded first.
Limitations and risks
- Extracts wrong values regularly: a near-miss amount, the order number instead of the invoice number, the due date instead of the invoice date.
- Currency is guessed from symbols and context; a bare
$is ambiguous (USD, NAD, AUD, CAD). - Text extraction quality limits everything: scanned or photographed invoices need OCR first and were not evaluated.
- Trained only on synthetic layouts; unusual real-world layouts, other languages and multi-invoice files are not covered.
- The reply may not be valid JSON, or may have extra or missing keys; validate it.
- Small model on consumer hardware: no production-readiness, latency or robustness guarantees.
- Never pay, book or file anything based on this output without human review.
License and attribution
- Adapter: released under
apache-2.0(as configured by the publisher). - Base model:
ibm-granite/granite-4.0-1b, licenseapache-2.0. - Training data: synthetic, generated by this project.
- Evaluation data:
drew-ipp/invoice-extraction-benchmark, licensecc-by-4.0(attribution required); made by InvoiceParser Pro, which also sells an extraction product. - The code, data and model licenses are independent; check each before reuse.
Reproducibility
The project that produced this adapter keeps every setting in configs/ (data.yaml, training.yaml, evaluation.yaml, publishing.yaml) and drives everything through make:
make install
make data-prepare # generate training data, extract the benchmark PDFs
make data-validate
make train PROFILE=m1_air_8gb
make evaluate-base # baseline
make evaluate # base + adapter
make card
make publish-dry-run
Artifacts: adapter in artifacts/adapters/m1_air_8gb, run manifest run_manifest.json (configuration, software versions, losses, timings), evaluation records under reports/evaluation/. Hardware: an Apple Silicon Mac; the m1_air_8gb profile targets an 8 GB M1 MacBook Air.