factusight-2B (GGUF)

A fine-tune of Qwen/Qwen3.5-2B that reads an invoice or receipt image (or the pages of a PDF) and returns its key data as a flat JSON object: invoice number, dates, issuer and receiver (name, tax ID, address, email, phone), subtotal, VAT, Spanish IRPF withholding, total, description, payment method and currency. It is the Flash model of FactuSight, a desktop application that extracts invoice data 100 % offline, with llama.cpp on the user's own machine.

  • Author: Manuel García Sanz · Website: https://factusight.com
  • Format: GGUF, Q4_K_M (1,274.4 MB) + vision projector (mmproj) F16 (668.2 MB)
  • In FactuSight: Flash mode (Modo Flash), the fastest tier, included in the Individual (desktop) edition
  • Speed: about 1.5 s per one-page invoice on an RTX 5070 Ti, using ~2.9 GB of VRAM (details below)
  • Runs with: llama.cpp (llama-server, llama-cli), fully offline
Model FactuSight tier Q4_K_M + mmproj VRAM (approx.) Use it when
factusight-2B Flash 1.94 GB ~2.9 GB low-end GPUs or CPU only, maximum speed
factusight-4B Standard 3.38 GB ~4.7 GB the balanced default
factusight-9B High Precision 6.55 GB ~7.4 GB GPUs with 8 GB or more, hardest documents

All three share the same prompt contract, output format and client script; only the size changes.

Resumen en español

factusight-2B es un ajuste fino de Qwen3.5-2B que lee la imagen de una factura o un ticket (o las páginas de un PDF) y devuelve sus datos clave en un JSON plano: número de factura, fechas, emisor y receptor (nombre, NIF/CIF, dirección, email, teléfono), base imponible, IVA, retención de IRPF, total, descripción, forma de pago y moneda. Es el modelo Flash (Modo Flash) de FactuSight, una aplicación de escritorio que extrae los datos de las facturas sin conexión, con llama.cpp en el propio equipo: ningún documento sale de la máquina. Está pensado sobre todo para facturas españolas (IVA, IRPF, NIF/CIF/NIE). Para usarlo por tu cuenta, arranca llama-server con el modelo y su mmproj y ejecuta python factusight_extract.py factura.pdf (instrucciones abajo). El prompt es fijo: el modelo se entrenó con ese contrato exacto. Puede equivocarse: revisa siempre los datos antes de contabilizarlos. Licencia CC BY-NC 4.0 (uso no comercial); para uso comercial, escribe a contact@factusight.com.

What it is and intended use

 invoice.pdf / .jpg / .png
          │
          ▼
 ┌──────────────────────────┐   PDF: first 3 pages, rendered at 300 DPI
 │ 1. render pages          │   (optional: embedded PDF text as auxiliary context)
 └────────────┬─────────────┘
              ▼
 ┌──────────────────────────────────────────────────────────┐
 │ 2. prompt = fixed system prompt                          │
 │            + page images                                 │
 │            + requested fields + field-specific rules     │
 └────────────┬─────────────────────────────────────────────┘
              ▼
 ┌──────────────────────────┐
 │ 3. factusight-2B         │   llama.cpp + mmproj, thinking disabled
 └────────────┬─────────────┘
              ▼
   flat JSON: up to 22 fields, each a string or null
              │
              ▼  (inside the FactuSight app only)
   validation heuristics ─► review screen ─► encrypted local DB ─► CSV / XLSX
  • One task, one contract. The model was trained only to answer the prompt described below with a JSON object. It is not a chat model and does not explain or reason.
  • Field selection. You ask for any subset of the 22 fields; the model answers with exactly those keys, in canonical order.
  • Honest nulls. A field that is not printed on the document comes back as null instead of being guessed (for example, no due date, no IRPF line, no email).
  • Normalized values. Amounts use a dot and two decimals ("1234.50"), dates use DD/MM/YYYY, tax IDs keep their letters.
  • Local. Everything runs on your machine; no document or result is sent anywhere.

Example

Example invoice (sample document with fictitious customer data)

Sample invoice with fictitious customer data, also published as examples/pccomponentes.pdf. Note: this layout was part of the training data, so it illustrates the input and output format; it is not a measure of accuracy.

Real output of factusight-2B (Q4_K_M, image only, all 22 fields) with the command in Quick start:

{
  "invoice_number": "402024/042028",
  "issue_date": "20/11/2024",
  "due_date": null,
  "issuer_name": "PC Componentes y Multimedia SLU",
  "issuer_tax_id": "B73347494",
  "issuer_address": "Avda Europa, 2-3, Pol.Ind. Las Salinas, 30840, Alhama de Murcia, Murcia",
  "issuer_email": null,
  "issuer_phone": null,
  "receiver_name": "Garcia Perez, Fran",
  "receiver_tax_id": "15087658A",
  "receiver_address": "Celso Emilio Ferreiro 71, Población: Zaragoza, CP: 50695",
  "receiver_email": null,
  "receiver_phone": "635336688",
  "subtotal": "247.85",
  "tax_rate": "21.00",
  "tax_amount": "52.05",
  "irpf_tax_rate": null,
  "irpf_tax_amount": null,
  "total": "299.90",
  "description": "AOC Q27G3XMN/BK 27\" Mini LED VA QHD HDR1000 180Hz, Abono de portes, Portes",
  "payment_method": "Paypal",
  "currency": "EUR"
}

Files

File What it is Size (bytes) SHA-256
factusight-2B-1.0.0-Q4_K_M.gguf Language model, Q4_K_M 1,274,396,704 877ad8281f73e0aaa8a697ba2010e9cfdd87241d963c7b85fffa96b9372c7b68
factusight-2B-1.0.0-mmproj-F16.gguf Vision projector (mmproj), F16. Required 668,226,816 f48cef0ebdfda0b1e704510e94138de4d66ac7dc03bdc842065ba7bb3c8c3888
factusight_extract.py Reference client for llama-server (stdlib only); --print-prompt prints the exact user prompt for any fields and page count
prompts/system.txt System prompt, exact text
prompts/user_all_fields.txt User prompt for a one-page document, all 22 fields
examples/pccomponentes.pdf Sample invoice used above
LICENSE, NOTICE, LICENSE.Qwen3.5 CC BY-NC 4.0; base model notice (Apache-2.0)
sha256sum factusight-2B-1.0.0-Q4_K_M.gguf factusight-2B-1.0.0-mmproj-F16.gguf

These are byte-for-byte the files used by the FactuSight application. Use the F16 mmproj: it is the one the application uses and the one tested here.

Quick start

Tested with llama.cpp b8664 (CUDA). Any recent build with Qwen3.5 vision support and the --image-max-tokens option should work.

Download:

pip install -U huggingface_hub
hf download manuelgscom/factusight-2B-GGUF --local-dir factusight-2B
cd factusight-2B

Option A: llama-server + the reference client (recommended)

llama-server -m factusight-2B-1.0.0-Q4_K_M.gguf \
  --mmproj factusight-2B-1.0.0-mmproj-F16.gguf \
  --jinja --reasoning off -c 12288 -ngl 99 --image-max-tokens 1280 --port 8080
pip install pypdfium2 pillow          # only needed for PDF input
python factusight_extract.py examples/pccomponentes.pdf
python factusight_extract.py my_ticket.jpg --fields invoice_number,issue_date,total,payment_method
python factusight_extract.py my_invoice.pdf --with-pdf-text   # also send the PDF's embedded text

factusight_extract.py renders PDFs at 300 DPI (first 3 pages), builds the exact prompt for the fields you ask for, calls the OpenAI-compatible endpoint /v1/chat/completions and prints the JSON. Read it to integrate the model into your own code: it is a single short file that depends only on the Python standard library (plus pypdfium2 for PDFs).

Option B: one shot with llama-cli

For a one-page image with all 22 fields, the prompt files are ready to use:

pdftoppm -r 300 -png -singlefile examples/pccomponentes.pdf page   # -> page.png

llama-cli -m factusight-2B-1.0.0-Q4_K_M.gguf \
  --mmproj factusight-2B-1.0.0-mmproj-F16.gguf \
  -ngl 99 -c 12288 --image-max-tokens 1280 --jinja --reasoning off \
  --temp 0.2 --top-k 10 --top-p 0.9 --seed 42 -n 2048 \
  -sysf prompts/system.txt --image page.png -p "$(cat prompts/user_all_fields.txt)" \
  -st --simple-io --no-display-prompt

Other fields or several pages. The user prompt changes with the fields you ask for (and with the page count), so generate it with --print-prompt, which only prints the text and needs no server. Pass the pages in order, comma-separated:

python factusight_extract.py --print-prompt --pages 2 \
  --fields invoice_number,issue_date,issuer_name,issuer_tax_id,total > user.txt

llama-cli -m factusight-2B-1.0.0-Q4_K_M.gguf \
  --mmproj factusight-2B-1.0.0-mmproj-F16.gguf \
  -ngl 99 -c 12288 --image-max-tokens 1280 --jinja --reasoning off \
  --temp 0.2 --top-k 10 --top-p 0.9 --seed 42 -n 576 \
  -sysf prompts/system.txt --image page1.png,page2.png -p "$(cat user.txt)" \
  -st --simple-io --no-display-prompt

-n is the output-token cap the application uses, clamp(256 + 64 × fields, 512, 2048): 576 for 5 fields, 2048 for 22. With a PDF as argument (python factusight_extract.py invoice.pdf --print-prompt --with-pdf-text), the page count and the PDF text are taken from the file.

Without a GPU, use -ngl 0: it works, but expect tens of seconds per document.

Prompt contract

The model was trained on this format and nothing else. Change it and accuracy drops.

Chat structure

Qwen3.5's own chat template (--jinja), thinking disabled (--reasoning off, or chat_template_kwargs: {"enable_thinking": false}), and exactly two messages:

  1. system: the fixed system prompt.
  2. user: first one image per page (at most 3, in order), then one text part.

System prompt

Exact text (ends with a newline), also in prompts/system.txt:

You are a document data extraction system.
Analyze the provided invoice/receipt image and extract the requested fields.
Return ONLY a valid JSON object with the field IDs as keys.
Do NOT include any explanation, markdown formatting, or additional text.

General rules:
- If a field is not found in the document, set its value to null. Do not guess or invent values.
- Numbers: use dot as decimal separator, two decimal places, no thousand separators.
- Dates: format as DD/MM/YYYY.

User text

Up to three blocks, joined with a blank line ("\n\n"):

┌───────────────────────────────────────────────────────────────┐
│ [only if pages > 1]  multi-page notice                        │
├───────────────────────────────────────────────────────────────┤
│ [optional]           embedded PDF text, as auxiliary context  │
├───────────────────────────────────────────────────────────────┤
│ [always]             field list  +  field-specific rules      │
└───────────────────────────────────────────────────────────────┘

1. Multi-page notice (only for 2 or 3 pages; {N} is the page count):

This invoice/receipt spans {N} pages. The images above are consecutive pages of the same document. Extract data from ALL pages.
IMPORTANT: The invoice data (issuer, receiver, amounts) is typically on page 1. Ignore contract terms, privacy policies, legal notices, or appendices on subsequent pages — do NOT use them to identify the issuer or receiver.

2. PDF text (optional; for digital PDFs). Image-only works well, as in the example above. If you add it, the sentence in brackets goes only for multi-page documents, and each page's text is preceded by a [Page N] line:

Use the image as the source of truth. The extracted PDF text below is auxiliary and may be incomplete or noisy.[ For multi-page documents, the text includes [Page N] markers to show which PDF page each text block comes from.]

--- Extracted PDF text ---
{text}
--- End of extracted PDF text ---

3. Field list and rules. The heading, then one line per requested field in the canonical order of the table below (whatever order you ask for them in), ending with a newline:

Extract the following fields from this document:
- "invoice_number": Invoice identification number
- "issue_date": Date when the document was issued
...
# Field ID Description line (exact text after - "id": ) Value
1 invoice_number Invoice identification number text
2 issue_date Date when the document was issued DD/MM/YYYY
3 due_date Payment deadline date DD/MM/YYYY
4 issuer_name Name or business name of the issuer text
5 issuer_tax_id Tax identification number of the issuer (seller/supplier issuing the invoice). CIF/NIF/DNI etc. text
6 issuer_address Full postal address of the issuer (including postal code, city, province/state, country, etc.) text
7 issuer_email Email address of the issuer text
8 issuer_phone Phone number of the issuer text
9 receiver_name Name or business name of the receiver text
10 receiver_tax_id Tax identification number of the receiver (customer/buyer/bill-to party). CIF/NIF/DNI etc. text
11 receiver_address Full postal address of the receiver (including postal code, city, province/state, country, etc.) text
12 receiver_email Email address of the receiver text
13 receiver_phone Phone number of the receiver text
14 subtotal Amount before taxes amount "0.00"
15 tax_rate VAT percentage applied percent "21.00"
16 tax_amount VAT amount amount
17 irpf_tax_rate IRPF withholding percentage applied (Spanish income tax retention) percent
18 irpf_tax_amount IRPF withholding amount (Spanish income tax retention) amount, positive
19 total Total invoice amount (subtotal + taxes - IRPF withholding if present) amount
20 description General description of the service or product text
21 payment_method Payment method indicated text
22 currency Document currency (EUR, USD, etc.) ISO code

Then, if at least one rule applies, a blank line, the heading IMPORTANT — field-specific rules: and only the rules whose trigger fields were requested, in this order, one per line, ending with a newline:

Field-specific rules (exact text) and their triggers
Included if you request Rule line
issue_date - issue_date: Look for words such as issue date, invoice date, or billing date.
due_date - due_date: Look for words such as due date, deadline, vencimiento, etc. If you DO NOT found them, return null
payment_method - payment_method: return the instrument (KEYWORDS: card, transfer, PayPal, cash, direct debit, RedSys, Tarjeta, Domiciliación SEPA, etc). Do NOT return status phrases (paid, completed, abonado, pagada). Rule: If you find a KEYWORD near amounts or payment terms, that's the payment method. If only status is present, then return null.
subtotal - subtotal: net amount after discounts/offers, before tax (base imponible). If the document shows both a gross/merchandise amount and a separate net amount (after discounts), always return the net amount. Look for labels such as 'Net Amount', 'Base Imponible', 'Importe Neto', 'Neto', 'Subtotal'.
tax_rate or tax_amount - tax_rate and tax_amount: return the value ONLY if a numeric tax rate or tax amount is explicitly printed on the document. If the tax line shows 0% or 0.00, return that value. But if the document only states it is tax-exempt (e.g. 'exenta de IVA', 'VAT exempt') without printing a numeric rate or amount, or if no tax line is visible at all, return null — do NOT infer or calculate a value.
irpf_tax_rate or irpf_tax_amount - irpf_tax_rate and irpf_tax_amount: return the value ONLY if a numeric IRPF withholding rate or amount is explicitly printed on the document. If no IRPF or withholding line is visible at all, return null — do NOT infer 0 or 0.00. Always return positive values (e.g. 275.06, not -275.06).
any issuer_* or receiver_* field - issuer_* data and receiver_* data: If you find a block that is labeled with or near words such as 'factura para', 'facturar a', 'bill to', 'customer', or similar, that block is ALWAYS the receiver, and the other block is ALWAYS the issuer. Ignore footer/legal/GDPR/provider text and any company mentioned only there. If the invoice was generated by an invoicing platform (e.g. MiAutónomo, FacturaDirecta, Holded, Quaderno), the issuer is the person or company that bills, NOT the platform — ignore platform branding, legal footers, and DPO/privacy contacts.
issuer_name or receiver_name - issuer_name and receiver_name: return null if no explicit person or company name is found. Do NOT use a tax ID, email address, or other identifier as a substitute for a name.
issuer_phone or receiver_phone - issuer_phone and receiver_phone: If you find 9 digit numbers (without letters) near the party block, that is the phone number of the party. If you find more than one phone number for one party, assign the first one you find.
issuer_email or receiver_email - issuer_email and receiver_email: must contain an '@' symbol.
issuer_tax_id or receiver_tax_id - issuer_tax_id and receiver_tax_id: A tax ID (CIF/NIF/DNI/NIE) is an alphanumeric code like 12345678A, B12345678, or X1234567L — it is NOT part of the address. If you see such a code in a party block, extract it as the tax_id. Include the country/letters prefix or suffix. Assign each party's own tax ID - do not swap or duplicate it if one is missing.
total - total: return the invoice total (subtotal + taxes - IRPF). Do NOT return the 'Amount Due' or balance remaining after payment — on paid invoices this is often 0.00, which is NOT the total.
invoice_number - invoice_number: return only the number/code, without labels such as Invoice, Factura, Nº, Núm., No., or #.

prompts/user_all_fields.txt is the complete user text for a one-page document with all 22 fields; python factusight_extract.py --print-prompt --fields ... --pages N prints any other combination exactly.

Sampling

The values used by the FactuSight application:

Parameter Value
temperature 0.2
top_k / top_p 10 / 0.9
repetition penalties none
seed 42
max output tokens clamp(256 + 64 × number_of_fields, 512, 2048)
context 12288 tokens
image tokens up to 1280 per page (--image-max-tokens 1280)
reasoning / thinking off
grammar / JSON schema none needed

Output

One JSON object, pretty-printed, with exactly the requested keys in canonical order. Every value is a string or null: no numbers, no nested objects, no line items.

  • Amounts and percentages: dot as decimal separator, two decimals, no thousands separator ("1234.50", "21.00"). IRPF values are positive.
  • Dates: DD/MM/YYYY.
  • null when the field is not printed on the document.

The FactuSight application parses the answer tolerantly (it strips code fences or stray text if they ever appear) and then runs its own validation heuristics: tax-ID plausibility, tax maths consistency, phone and email checks, tax IDs found inside addresses, and so on. If you integrate the model yourself, validate its output too, especially amounts and tax IDs, before using it for accounting.

Hardware and speed

Measured by the author with factusight_extract.py on the example invoice above (one page, all 22 fields, image only), llama.cpp b8664, all layers on the GPU, warm server:

Model GPU Time per invoice VRAM used
factusight-2B RTX 5070 Ti 16 GB ~1.5 s ~2.9 GB
factusight-4B RTX 5070 Ti 16 GB ~2.5 s ~4.7 GB
factusight-9B RTX 5070 Ti 16 GB ~4.2 s ~7.4 GB

Times grow with the number of pages and fields. A rough guide for VRAM: model file + mmproj + about 1 GB. The models also run on CPU or with partial offload (-ngl N), more slowly.

Training

Base Qwen/Qwen3.5-2B (loaded from the unsloth/Qwen3.5-2B mirror)
Method LoRA in bf16 (not QLoRA) on the vision encoder and the language model (attention + MLP), merged into the weights. 23.1 M (1.03 %) trainable parameters
LoRA rank 16, alpha 16, dropout 0
Hyperparameters lr 1e-4 (linear, 10 warmup steps), 8-bit AdamW, weight decay 0.01, effective batch 8 (8 per device × 1 accumulation), 1 epoch (451 steps), max sequence length 8192, 256–1280 image tokens per page; loss on the JSON answer only
Software Unsloth 2026.4.1, TRL SFTTrainer, Transformers 5.5.0, PyTorch 2.10 (CUDA 12.8)
Hardware 1× RTX 5090 (32 GB), 51 minutes
Export Merged, converted to GGUF and quantized to Q4_K_M; vision projector exported in F16

Data. 3,608 training examples built from 1,052 documents: 1,000 synthetic invoices and receipts plus 52 real invoices with their data modified, reviewed by hand. Each document is paired with 4 prompts that ask for different field subsets (5–8, 9–13, 14–18 and all 22 fields), so the model learns to answer exactly what is asked. The synthetic documents are rendered from HTML templates (48 template packs, 14 kinds of business: telecom, SaaS, utilities, restaurants, fuel stations, transport, hotels, marketplaces…) at 300 DPI, mostly in Spanish with some English and mixed documents, one to three pages, about 40 % in greyscale, a mix of digital PDFs (with and without usable text) and scanned-looking images, and about 20 % with mild degradation (small rotation, blur, contrast and brightness changes, edge shadows, JPEG recompression). Spanish tax content throughout: VAT, IRPF withholding, NIF/CIF/NIE. A separate set of 600 examples from 150 held-out synthetic documents was used for validation during training. The training data and the training recipe are not published.

Limitations

  • Spanish invoices first. Trained mainly on invoices issued in Spain (VAT, IRPF, NIF/CIF/NIE). Other countries, languages and tax systems work only partially; simplified receipts and tickets are less covered than full invoices.
  • Fixed set of fields. 22 header-level fields only: no line items, no multiple tax rates in one document (one tax_rate / tax_amount), no free-form questions.
  • Up to 3 pages, as trained. Longer documents should be trimmed to the pages that hold the invoice data.
  • It can be wrong. Misread digits, swapped parties or a missing field are possible, more often with blurry photos, handwriting or unusual layouts. Always review the data before using it for accounting or tax purposes. It is not tax advice.
  • One prompt, no thinking. Other prompts, chat use or reasoning mode are unsupported.
  • Requires the mmproj. The GGUF alone is text-only; without the vision projector the model cannot see the document.

Changes from the base model

  • Fine-tuned with LoRA (merged) for the contract above.
  • Tokenizer and chat template unchanged from Qwen3.5-2B.
  • Converted to GGUF, quantized to Q4_K_M; vision projector in F16; general.* metadata rewritten (name FactuSight-Flash, author FactuSight, version 1.0.0).

License

The weights in this repository are released under CC BY-NC 4.0: free to use, share and adapt for non-commercial purposes, with attribution. For commercial use, contact contact@factusight.com.

The base model, Qwen3.5-2B by the Qwen team (Alibaba Cloud), is licensed under the Apache License 2.0; its license text is in LICENSE.Qwen3.5 and the attribution in NOTICE.

Citation

@misc{factusight_2b_2026,
  title        = {factusight-2B: a fine-tuned Qwen3.5-2B for offline invoice data extraction},
  author       = {García Sanz, Manuel},
  year         = {2026},
  howpublished = {\url{https://huggingface.co/manuelgscom/factusight-2B-GGUF}}
}
Downloads last month
-
GGUF
Model size
2B params
Architecture
qwen35
Hardware compatibility
Log In to add your hardware

4-bit

Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for manuelgscom/factusight-2B-GGUF

Finetuned
Qwen/Qwen3.5-2B
Finetuned
(623)
this model