ganmoor-ai-labs/finanalyzer-indian-bank-statements
FinAnalyzer — Indian Bank Statement Extraction Model
A 4B-parameter model that turns the text of any Indian bank or credit-card statement into a structured, machine-verifiable transaction ledger — running fully offline on a phone or a small server.
FinAnalyzer is the extraction core of an offline-first personal-finance app. It reads a statement page and emits strict JSON: account header + a list of transactions (ISO date, verbatim narration, debit/credit, amount, running balance). Because the most sensitive document most people own is their bank statement, the design goal is that nothing ever leaves the device — the model is small enough to run on-device, and a separate deterministic checker verifies the arithmetic so the language model never has to be trusted with a sum.
- Base model: Qwen/Qwen3-4B-Instruct-2507 (Apache-2.0)
- Method: LoRA (rank 16, all attention + MLP projections), assistant-only loss, thinking disabled
- Training data: 10,000 synthetic Indian statements (no real customer data — see Training data)
- Formats:
Q8_0(4.0 GB, recommended) andQ4_K_M(2.4 GB, phone-class) GGUF
What it extracts
Per statement page, a single JSON object:
{
"bank": "STATE BANK OF INDIA",
"doc_type": "savings",
"account_number": "69666708937",
"holder": "AMIT KULKARNI",
"ifsc": "SBIN0009059",
"period": {"from": "2026-10-01", "to": "2026-10-31"},
"opening_balance": 202552.41,
"closing_balance": 211678.35,
"transactions": [
{"date": "2026-10-03", "narration": "BY DEBIT CARD-OTHPOS531388VIMTA LABS--",
"ref": null, "type": "debit", "amount": 420.53, "balance": 202131.88}
]
}Credit-card statements emit card_number_masked, total_due, min_due, credit_limit instead of the balance fields. Dates are normalised to ISO-8601; amounts are numbers (no ₹, no lakh commas); multi-line narrations are rejoined; absent fields are null.
The trust layer (why the model never does arithmetic)
Language models miscount. FinAnalyzer's contract is that the model only transcribes — every number it emits must already appear in the source text. A separate deterministic checker then verifies the ledger:
opening + Σcredits − Σdebits == closing(±0.01)- every row's
prev_balance ± amount == balance - dates within the statement period, monotonic
If any invariant fails, the extraction is flagged for review. This check is pure code, ships with the model, and is what lets a downstream app trust the ₹ figures absolutely.
Results
Transaction-level exact match = every field of a transaction (date, narration, type, amount, balance) correct. Value match = the ledger-critical subset (date, type, amount, balance) correct — narration text ignored.
In-distribution (supported banks, unseen customers)
Supported banks: HDFC, SBI, ICICI, Axis (savings) + HDFC, ICICI (credit card).
Out-of-distribution generalisation (banks never seen in training)
The numbers that carry money (amount, balance, date) generalise near-perfectly even to unseen layouts; verbatim narration is the only field that softens.
Independent external benchmark (AgamiAI/Indian-Bank-Statements)
A completely independent generator — fictional banks, business current accounts, narration styles never in our training (By Clg:…). ~6,500 transactions across 40 multi-page statements.
Perfect count parity (no dropped or hallucinated rows) and ~94–95% value accuracy against a generator we never touched.
Quantization guidance
Q8_0 is near-lossless and is the recommended build (held-out 94.9% ≈ bf16 94.5%). Q4_K_M is near-lossless on supported banks (99.6%) but degrades debit/credit classification on unsupported layouts (held-out exact drops to ~80% because type inference weakens on formats like single-column DR/CR flags). Use Q4_K_M for phone deployment against banks you've trained/validated; use Q8_0 when robustness to unknown layouts matters.
How to use
# serve (llama.cpp, OpenAI-compatible). Build with CUDA for GPU.
llama-server -m finanalyzer-q8_0.gguf --port 8091 -c 6144 --parallel 1 -ngl 99The model expects one page of statement text per request, with this system prompt:
You are a bank-statement extraction engine. Input: the text of one page of an Indian bank or credit-card statement. Output: ONLY a JSON object, no other text. For the first page include: bank, doctype (savings|current|creditcard), accountnumber (or cardnumbermasked), holder, period {from,to} (ISO dates), openingbalance and closingbalance (savings) or totaldue, mindue, creditlimit (credit_card), and transactions. For other pages include only transactions. Each transaction: date (ISO), narration (verbatim, join wrapped lines with a space), ref (or null), type (debit|credit), amount (number), balance (number, null if absent). Never invent values not present in the text; use null when absent.
User: Statement page 1 of 1:
<pdftext of the statement page>Extract the PDF text layer (e.g. pdfplumber) page by page, send each page, and concatenate the transactions. Then run the invariant check before trusting the ledger.
Training data
100% synthetic, generated by a purpose-built pipeline — no real customer statement was used, so nothing memorisable is private:
- Persona-driven transaction streams (salary, rent, EMIs, SIPs, UPI habits, ATM, reversals) with balance-consistent running totals.
- Per-bank narration grammars reproducing the real narration shapes of each bank (UPI/NEFT/IMPS/POS/ACH/clearing conventions), filled with checksum-valid fake account numbers, IFSC codes, and public brand names (brand names are facts about which businesses exist, not personal data).
- Distinct PDF layouts per bank (column schemes, date formats, fonts) rendered to real text-layer PDFs.
- Free ground truth: because the generator knows every value it placed, each statement ships with exact JSON labels; train/test use disjoint layouts, templates, and value pools so evaluation measures true generalisation.
10,000 statements → ~19k page-level training examples. Held-out banks (Kotak, Canara) and the external AgamiAI set were never in training.
Limitations & scope
- Digital (text-layer) PDFs only. Scanned/photographed statements need an OCR front-end (e.g. Surya) feeding this model; that path is not evaluated here.
- Supported banks are the six above; other banks work via generalisation (see held-out results) but are not guaranteed. Adding a bank means adding its layout to the generator and continuing training.
- Verbatim narration on unseen banks/dialects is the weakest field (~96%); the numeric ledger is far more robust. For a downstream categoriser this is immaterial (categorisation tolerates paraphrase).
- Not financial advice. This model extracts and structures; it does not advise.
- Trained and evaluated in English + Hindi/Hinglish narration contexts.
- Speed is hardware-bound: on unified-memory devices, single-stream decode is memory-bandwidth-limited (~40 tok/s for Q8 on a GB10-class chip).
Positioning
Cloud statement-analysis tools face a paradox: you upload your most sensitive document to a server to have sensitive data processed. FinAnalyzer is built to run entirely on-device — the privacy claim is architectural, not a promise. Amounts and balances are verified by deterministic code, not asserted by a language model.
Citation / provenance
Built on Qwen3-4B-Instruct-2507. Synthetic-data + LoRA pipeline by Ganmoor AI Labs. Evaluated against the independent AgamiAI/Indian-Bank-Statements benchmark. Apache-2.0.
