The short answer
- What works: a vision-language model (VLM) that reads each page image straight into rows (date, narration, debit, credit, balance). Our own code then checks every row. The best models read 97–99% of rows exactly on real scans. With the checks, almost no wrong row got through unflagged.
- Where: on our server, not on the customer's PC. Open models on a CPU take 38–130 s per page, far over a 10–12 s budget for a whole statement. Our server has 4 CPU cores and no GPU, so it must send pages to an API (or to a rented GPU later), all pages of a statement at the same time.
- Speed: the 10–12 s target is reachable only with every page read in parallel. The models write about 120–140 tokens a second, and a page needs 700–1,500 tokens, so the slowest page sets the time: about 6–13 s of writing, plus 1–3 s to send the image. Dense pages need the shorter output format and split pages described below. My CLI-based tests could not measure the true API time (see "Speed").
- Which service: start with Gemini 3.8 Flash (97.9% of rows exact on real scans, about $4 per 1,000 pages). Add Claude Haiku 5.5 as a cheap second reader (97.2%, about $1–4 per 1,000 pages). Claude Opus 5.5 was the most accurate (99.3%), at about $26 per 1,000 pages. Use it for pages that fail the checks.
- Open source: not good enough yet. On real scans PaddleOCR-VL-1.6 read 74% of rows exactly and GLM-OCR 77% (cloud models: 97–99%). PP-OCR (Apache-2.0) is still useful: it finds 99.8% of amounts exactly in 2–7 s per page on CPU, so it is our independent second reader and gives the position of each value on the page.
- What does not work: adding OCR text to our current parser. Only 1 of 37 OCR'd statements validated. Tesseract misses 22–69% of amounts.
What I tested on
There were no scanned uploads in production yet (12 statements in total, all text PDFs). So I built three test sets. Two of them have exact answers.
| Set | Where it came from | Pages | Rows | How we know the right answer | Sent to cloud? |
|---|---|---|---|---|---|
| Production | The 6 distinct validated production statements, turned into scan-like images (tilt, blur, noise, grey paper, JPEG) | 22 | 441 | The validated rows saved in production | No, this PC only |
| 31 banks | One text PDF per bank from the Drive samples (SBI, HDFC, ICICI, Axis, BoB, PNB, Kotak, co-operative banks and more), made scan-like in the same way | 31 | 704 | Our parser's validated rows from the original text PDF | Yes |
| Real scans | The 31 scanned PDFs in the Drive samples: phone photos, office scans, passbook pages, handwriting, ticks, punched holes, cut-off edges | 124 (31 scored) | 428 scored | Page 1 of each PDF: where two models from different vendors agreed exactly and the balances reconciled (26 pages), I accepted the rows. I checked the other 5 pages by eye. | Yes |
A row counts as right only if its date, debit, credit and balance all match exactly. Narration is scored separately. Because the real-scan answers came partly from model agreement, they favour models that agree with Gemini and Claude Sonnet. The two other sets have no such bias.
Results
| Engine | Type | Real scans | 31 banks | Production | Seconds per page | USD per 1,000 pages | License / access |
|---|---|---|---|---|---|---|---|
| Cloud APIs (tested through their command-line tools) | |||||||
| Claude Opus 5.5 | General VLM | 100% | not sent | 17* | ~26 | Anthropic API; India processing on Bedrock | |
| GPT-6 Astra | General VLM | 99.9% | not sent | 28* | ~54 | OpenAI API; no India processing | |
| Gemini 3.8 Flash (low thinking) | General VLM | 100% | not sent | 29* | ~4 | Google API; Vertex AI Mumbai (check model list) | |
| Claude Haiku 5.5 | General VLM | 98.6% | not sent | 20* | 1–4 (measured) | Anthropic API | |
| Gemini 3.8 Flash (high thinking) | General VLM | 100% | not sent | 109* | ~4 | One 46-row page came back empty | |
| Claude Sonnet 5.5 | General VLM | 100% | not sent | 15* | ~13 | Slipped a date column by one row on one page | |
| Open models on CPU (8 threads of a Ryzen 9 9950X) | |||||||
| GLM-OCR (official table prompt) | OCR VLM, 0.9B | 100% (2 pages) | — | 112 | 0 | MIT. Stopped after 19 of 31 real-scan pages: too slow | |
| PaddleOCR-VL-1.6 (official pipeline) | Layout + OCR VLM, 0.9B | — | — | 38 | 0 | Apache-2.0. Real scans only; then stopped | |
| Classic OCR on CPU (text only, no rows) | |||||||
| PP-OCRv6 via RapidOCR | Detector + recognizer | — | amounts found exactly: 99.8% | 2–7 (4 cores) | 0 | Apache-2.0; runs on Windows and Linux | |
| Tesseract 5 | Detector + recognizer | — | amounts found: 78% / 31% | 2–4 | 0 | Apache-2.0 | |
| PP-OCR words fed into our current parser | OCR + recipes | — | 58% | 57% | +1 | 0 | Validated 1 of 37 statements |
* Cloud times include the start-up of each command-line tool (10–20 s). A direct API call should take about 5–15 s per page, and pages of one statement can run at the same time. Costs are API list prices (October 2026) times tokens per page. Haiku was measured: 2,900–7,500 output tokens per page, because thinking is on by default and is billed as output. The other costs count only the visible rows (about 820 tokens) and are too low by the same effect: treat them as minimums. "Production" was run only with engines on this PC.
Speed against a 10–12 s budget
The budget is for a whole statement, from upload to rows. Most scanned statements in the samples have 1–4 pages; the longest has 23.
| What I measured | Result | What it means |
|---|---|---|
| Open models on CPU, per page | 38–130 s | Out on any PC or on our server |
| Model writing speed (fitted from 400+ page calls) | 120–140 tokens/s | Haiku, Sonnet and Gemini Flash are alike; GPT-6 Astra is about 54 tokens/s |
| Output per page | 700 median, 1,530 for dense pages (tokens) | Writing alone takes 6–13 s per page |
| CLI start-up with no image (cost of my test method only) | 4.5 s Claude, 7.5 s Codex, 15 s Antigravity | A direct API call does not pay this |
| 10-page real scan, all pages at once, through the CLI | 22 s Haiku, 59 s Gemini | Includes the CLI overhead and a file-reading step for each page |
| Same statement, one line per row instead of JSON | 20% fewer characters | Accuracy unchanged (224–226 of 226 rows agree) |
| Same statement, each page cut into two overlapping halves | Output per call down 42% | All 226 rows recovered after merging the halves |
Estimate for a direct API call: 1–3 s to send the image, then the writing time of the slowest page. A typical page should take about 7–9 s and a dense page about 13–15 s. With the one-line format and split pages, a dense page drops to about 5–8 s. With every page sent at once, a statement of up to about 20 pages should finish in 8–12 s. This is an estimate, not a measurement. To confirm it, I need a direct Gemini or Anthropic API key, or the GPU free for the local test.
Two more ways to make the wait feel shorter: show rows as they arrive (pages finish one by one), and start reading as soon as the upload ends, before the bank is confirmed.
The mistakes the models made
All errors on the real scans fall into five kinds. These are the cases our checks must handle.
- A digit misread. GPT-6 Astra read a 6 as a 0 in an amount. The balance check flags it, and PP-OCR read the digit correctly.
- A guess where the page hides the value. A punched hole hid the first digit of a date. Gemini wrote a full date; Sonnet and Opus wrote "?". Where the dates on both sides fix the day (a date between 08/05 and 13/05 that ends in 3 must be 13/05), the guess was right. In one case it was not knowable. No automatic check can catch a plausible guess, so the prompt must ask for "?" and the app must show such rows.
- Paise invented on a cut-off page. The scan cut the balance column after the rupees. Sonnet wrote ".00", Gemini worked out the paise by arithmetic, and only GPT wrote "?". A check that PP-OCR also saw the paise digits would catch this.
- Rows shifted by one. On a page covered with handwritten notes, Gemini moved the amount column down one row. Sonnet did the same with the date column on another page. Shifted amounts break the balance on every row, which is easy to catch. Shifted dates do not touch the balance. A row with a date but no amount is the clue.
- A whole page missing. Gemini on "high" thinking returned nothing for a 46-row page. The row count and the balance across the page break show this.
What our checks catch
For each page I ran four checks that need no right answer:
- The running balance reconciles on every row (in page order or newest-first).
- Every amount the model read also appears in the PP-OCR reading of the same page.
- The dates never go backwards.
- Every row has a balance and exactly one of debit or credit, and no "?".
| Model | Wrong rows | Wrong rows that passed all checks | Amounts flagged by PP-OCR | Wrong amounts the PP-OCR check caught |
|---|---|---|---|---|
| Claude Opus 5.5 | 3 | 0 | — | — |
| GPT-6 Astra | 5 | 0 | 41 of 839 (4.9%) | 3 of 3 |
| Gemini 3.8 Flash | 9 | 1 (the punched-hole date) | 45 of 847 (5.3%) | 7 of 8 |
| Claude Haiku 5.5 | 12 | 0 | 37 of 847 (4.4%) | — |
| Claude Sonnet 5.5 | 36 | 0 | 38 of 847 (4.5%) | 0 of 8 (the invented paise) |
On whole statements the balance check is even stronger. Gemini 3.8 Flash read all 124 pages of the 31 real scans (2,155 rows). 27 of 31 statements reconciled from the first row to the last, across page breaks. The other 4 showed 45 breaks, all on pages with heavy handwriting or ticks. Those statements would open as "to check", with the exact rows marked, as the app already does for needs_review.
The PP-OCR check is strict: on real scans it questions about 5% of correct amounts, because PP-OCR itself misreads some cells. So it should mark single cells to check, not reject the page.
Where to run it
| Option | Speed (measured) | Accuracy | Cost | Verdict |
|---|---|---|---|---|
| Customer's Windows PC, CPU | PaddleOCR-VL-1.6: 32 s/page; GLM-OCR: 128 s/page (4 fast cores). A 10-page statement takes 5–20 minutes. Office PCs are slower. | Small open models only | Free, but a 1–2 GB download in the installer | No for reading rows. PP-OCR (about 2–7 s/page) could run here. |
| Customer's PC, GPU | Fast on a gaming GPU, but most office PCs have none | Same as the open models | Free | No: we cannot count on the hardware |
| Our server (4 vCPU EPYC 7F72, 15 GB, no GPU) | About 1.5–2× slower than my PC's cores: about 60 s/page for a 0.9B model | Same as the open models | Free | Only PP-OCR: a VLM is too slow for the 100 s Cloudflare limit |
| Our server → VLM API | 5–15 s per page; pages run in parallel | 97–99% of rows exact on real scans | $1–26 per 1,000 pages | Yes, start here |
| Our server → rented GPU with an open model | Not measured: the test GPU was busy. Published: about 1 page/s per model on an A100 | 74–77% on real scans for the open models tested (on CPU; same weights) | About $0.5–1 per hour for an L4-class GPU, or per-second serverless | Later: when volume is high or data must stay with us |
At today's volume an API is far cheaper than any GPU. 10,000 scanned pages a month cost about $40 with Gemini Flash plus about $6 for a Haiku second reading. A GPU that is always on costs $400–700 a month.
Proposed design
The design keeps the rule that already protects the app: a statement is validated only when its numbers prove themselves, and otherwise it is needs_review with the rows marked.
scanned_pdf instead of failing.validated. Else needs_review with the rows to check, and Alt+O shows the place on the page from the PP-OCR boxes.Steps to build it
- Ask the user (the accountant) for consent to send page images to the chosen provider. Today they consent only to sending extracted text to OpenAI.
- Add a scan path on the server: render pages, call Gemini 3.8 Flash per page with a fixed JSON schema and amounts as strings, at temperature 0.
- Run PP-OCR (RapidOCR, ONNX) on the server for the cross-check and the word boxes.
- Reuse our validation: balance chain, totals,
needs_reviewreasons, plus new reasons such asocr_disagreesandunreadable_value. - Retry failed rows once with Claude Opus 5.5 (or Haiku as a cheap second reader), then show the rest to the user.
- Make long statements asynchronous (a job with progress), because 20+ pages can pass Cloudflare's 100 s limit.
- Track every scan outcome in
tracking_events, so we can measure real accuracy.
External services
From web research on 17/10/2026. I tested only the general VLMs above; the rest are from the vendors' pages.
| Service | What it gives | USD per 1,000 pages | Processing in India | Fit for us |
|---|---|---|---|---|
| Gemini 3.8 Flash (Google) | Rows straight from the image (tested) | ~4 (doubles on 01/01/2027) | Vertex AI Mumbai for listed models; check 3.8 | First choice |
| Claude Haiku / Opus 5.5 (Anthropic) | Rows straight from the image (tested) | ~0.6 / ~26 | Amazon Bedrock India profile | Second reader / hard pages |
| GPT-6 Astra (OpenAI) | Rows (tested); most honest about unreadable values | ~54 | Storage only, no processing | Too costly |
| Mistral OCR 4 / 4.1 | Markdown and tables, block boxes, word confidence | 4 (2 in batch) | Not stated | Not tested: no API key |
| Google Document AI OCR | Words with positions | ~1.5 | Mumbai | Second reader option |
| AWS Textract (tables) | Words and table cells with positions | 15 | Mumbai | Second reader option |
| Azure Document Intelligence (Layout) | Words and tables with positions | ~10 | Central India | Its bank-statement model reads US statements only |
| Perfios, FinBox, Signzy, HyperVerge, Arya.ai | Bank statement analysis for lenders; accept scans | Quote only | India | No: built for credit reports, not rows for accountants |
| LlamaParse, Reducto, Unstructured, Nanonets, Docsumo | General document parsing | 1–300 | Not stated | No advantage over a VLM plus our checks |
Open-source models
The newest public ranking (OmniDocBench v1.6, September 2026) is led by small open models. None of the benchmarks uses Indian bank statements. I ran PaddleOCR-VL-1.6 and GLM-OCR on our real scans (74% and 77% of rows exact). The others are downloaded and ready for a GPU test.
| Model | Size | License | OmniDocBench v1.6 | Positions of values | Notes |
|---|---|---|---|---|---|
| TeleOCR | 1.2B | Apache-2.0 | 96.9 | Layout blocks | Built for photos of documents |
| OvisOCR2 | 0.8B | Apache-2.0 | 96.5 | Images only | |
| PaddleOCR-VL-1.6 | 0.9B | Apache-2.0 | 96.3 | Text spotting | Best on skew and photos (Real5 benchmark) |
| MinerU2.5-Pro | 1.2B | Apache-2.0 (model) | 95.8 | Layout blocks | Ready CPU install in MinerU 4 |
| GLM-OCR | 0.9B | MIT | 95.2 | No | |
| Chandra 2 | 4B | Paid above $2M revenue | — | Layout blocks | Best on old scans (olmOCR-Bench); not tested |
| Qwen3-VL 4B / 8B | 4–8B | Apache-2.0 | — | On request | General model: follows our JSON prompt |
| PP-OCRv5 / v6 | small | Apache-2.0 | — | Words and characters | Not generative: cannot invent a row |
Privacy
- Production statements never left this PC. Only local engines read them.
- A scan carries more than its text: names, full account numbers, signatures. Sending page images to Google or Anthropic needs its own consent from the accountant.
- To keep processing in India: Claude on Amazon Bedrock's India profile, Gemini on Vertex AI in Mumbai (check that 3.8 Flash is listed), or open models on our own GPU.
- The DPDP Rules on security and transfers abroad take effect around May 2027. Transfers abroad are allowed unless the government blocks a country. This is not legal advice.
Limits of this study
- Only 31 real scanned statements, mostly from Gujarat banks. The answers for real scans came partly from model agreement.
- Cloud models were called through their command-line tools, so the times include tool start-up. Costs are estimates.
- I did not test Mistral OCR, Textract, Azure or Document AI (no keys), or Chandra 2 (license, and too big for 8 GB).
- The local GPU tests did not run: the RTX 5060 was in use the whole time. The open models ran on CPU with the same weights. Their accuracy carries over; their GPU speed does not.
- My table-to-rows conversion for the open document models is simple. Part of their lower score may come from it, not from the reading.
How I ran the tests
All work is in the main checkout's ignored folder .local/ocr-research/ (scripts, sets, results). scripts/build_sets.py makes the sets, run_cli.py runs cloud models, run_vllm.py and run_openai_api.py run local ones, evaluate.py scores rows, gate.py measures the checks, doc_check.py checks whole statements, and probe_docs.py feeds PP-OCR words into the current parser. The earlier experiment in bank-parser/docs/ocr-sample-036-benchmark.md reached the same result for OCR plus the current parser.
Every model got the same prompt: return rows as JSON, copy digits exactly, never calculate, write "?" for unreadable characters, and ignore handwriting and stamps. Document models got their own table prompt, and I turned their tables into rows by their column headers.
Sources
- OmniDocBench leaderboard: github.com/opendatalab/OmniDocBench (updated 11/09/2026)
- PaddleOCR-VL-1.5 paper, Real5-OmniDocBench: arxiv.org/abs/2601.21957
- Gemini prices: ai.google.dev/gemini-api/docs/pricing; Claude prices: claude.com/pricing; OpenAI prices: developers.openai.com/api/docs/pricing (all read 17/10/2026)
- Mistral OCR 4: mistral.ai/news/ocr-4 (23/06/2026)
- Document AI regions: docs.cloud.google.com/document-ai/docs/regions; Textract prices: aws.amazon.com/textract/pricing
- Azure bank statement model (US only): learn.microsoft.com/azure/ai-services/document-intelligence/prebuilt/bank-statement
- Claude on Bedrock in India: aws.amazon.com/blogs/machine-learning/amazon-bedrock-expands-claude-model-availability-to-india-cross-region-inference (29/09/2026)
- Number hallucination in VLMs: aclanthology.org/2026.findings-acl.1237; "Do VLMs Read or Rewrite?": arxiv.org/abs/2607.21617
- OCR first, then VLM, on financial scans: arxiv.org/abs/2604.26462
- DPDP Rules 2025 timeline: scconline.com (26/12/2025)