Statements to Sheets logoStatements to Sheets
Built on
Google Cloud
← Back to Blog

Article

Making PDF Conversion Faster: Gemini Benchmarks and What We Changed

By Jeff Gough ·

Scanned statements were the slowest thing our converter did. Processing four pages of a 13-page scanned credit card statement took 2 minutes 15 seconds in production, and some uploads felt stuck. This article is the full account of what we measured, which Gemini models we compared, what the thinking setting did, what we changed in the code, and what extra CPU on the server bought. Every number below comes from our own runs. Where a test was small or I could not verify something, I say so.

Summary

  • Some scans arrive sideways or upside down. We added automatic straightening for them, and then made it cheaper: on the test pages, sideways scans went from an average of 33.7 seconds to 12.7 seconds.
  • Most of the time on a scanned page was OCR work done on the server, not AI calls. Finding out whether a page was scanned sideways cost 13 to 17 seconds on a two-page preview of one sideways scan.
  • We compared five Gemini models on identical inputs. A smaller, cheaper model with minimal thinking was 2.5 to 4 times faster than the larger model we were using and matched it on most pages.
  • More thinking did not improve document classification. Minimal thinking was the fastest setting and made no more routing mistakes than the others in our test set.
  • Three OCR changes cut the OCR work on 23 test pages from 379 seconds to 170 seconds with byte-identical text, word positions and rotation.
  • Giving the server more CPU helped more than our local tests predicted: four pages of the 13-page scan went from 2:15 on 1 CPU to 0:57 on 4 CPUs.

How a scanned statement gets converted

A PDF with a text layer can be read directly. A scan is a picture, so the converter has to run optical character recognition (OCR) to get text and word positions. The pipeline then does five things.

  1. Classify the document. An AI model looks at images of the first two pages and decides whether it is a bank statement (date, description, amount lines) or a column-based document such as a ledger or aging report.
  2. Straighten the page. Some scans are fed in sideways or upside down. Tesseract, the OCR engine, can guess the rotation, and the converter checks the guess before trusting it.
  3. OCR the page. The page is rendered at 400 DPI and Tesseract returns every word with its position.
  4. Parse rows. Bank statements are read line by line with pattern rules. Column documents are split into columns by the horizontal position of each word, using headers found by an AI model.
  5. Check each page. An AI model looks at the page image next to the extracted rows and repairs OCR mistakes such as an O read as a zero, and removes rows the user asked to drop.

Four of those steps call Gemini or run OCR, so they were the places to look for time.

The rotation fix

Scanners and phone apps sometimes save a page turned 90 degrees or upside down. A person fixes that by tilting their head. OCR does not: run on a sideways page, it returns scrambled characters, so the converter found few or no transactions, and the AI page check was shown a sideways image too. We added automatic straightening, and it works for every document type, not only bank statements.

  1. Trigger. Tesseract's orientation detection runs on a shrunken copy of the page (longest side 1,800 pixels). It returns a rotation of 0, 90, 180 or 270 degrees and a confidence score. If it says 0, or its confidence is below 0.3, the page is left exactly as scanned.
  2. Verify. The orientation guess is not trusted on its own, because on sparse pages, such as a mostly empty summary page, it can point the wrong way. Instead we run a quick OCR of the page as scanned and turned, and count the words that are both recognized with confidence of at least 50 and found in a list of common English and statement vocabulary ("balance", "deposit", month names and so on). Gibberish from a wrong orientation almost never lands in that list.
  3. Accept only a clear win. A turn is applied only if it finds at least 8 such words and at least 1.5 times as many as the page as scanned. Otherwise the page stays as it was.
  4. Use it everywhere. The page is turned upright before the full OCR. The rotation is also remembered, so the image sent to the AI page check is turned the same way, and the model sees the page the way the OCR read it.

In our 23-page test set, 4 pages were sideways, all needing the same 270-degree turn to read, on the first page of four scanned statements. The other 19 were upright and were never changed. These four were read as normal transaction pages after the fix. I did not keep a before-and-after count of rows for them, so I cannot give a number for how many transactions the fix recovered.

One limit: the first step of the pipeline, which decides whether a file is a bank statement or a column document, looks at the page images as they were scanned, without straightening them. We tried adding a straightening step there and removed it, because the classifier routed the sideways scans in our test correctly without it. That is a result on a small set, not a guarantee.

Where the time was going

We timed each stage on a two-page preview of a sideways-scanned credit card statement, using the real code path on a development machine.

StageTime
Classify the document (one Gemini call)about 5 s of a 7 s step
Bank preview, rotation check and OCR for two pagesabout 25 s
Of that, rotation checks13 to 17 s
AI page check (one Gemini call)about 3 s

The rotation check was the largest single cost. When Tesseract suggested that a page was rotated, the converter read the page four ways (as scanned, then turned 90, 180 and 270 degrees) and picked the orientation with the most recognizable words. It did this even when the first guess was clearly right. The column-based engine made things worse by running OCR on every scanned page twice: once to get word positions and again to get plain text.

Benchmarking the Gemini models

The method

We compared models on the column engine's page check, because it was the slowest AI call. Rather than re-run whole documents, we captured the exact inputs of 12 real page checks from 7 scanned documents (a general ledger, a trial balance, an accounts payable aging report, two other scanned reports and two credit card statements) and replayed each one against each model. The prompt, the page image and the extracted rows were the same every time, so only the model changed.

We measured time per page, whether the model kept every amount it was given, and whether it invented new ones. Models: gemini-3.8-flash (what we used), gemini-3.5-flash-lite, gemini-3.1-flash-lite and gemini-2.5-flash.

Speed

ModelMean per pageLargest page (108 rows)Result
3.8-flash, low thinking18.6 s89.7 sRepaired OCR errors
3.5-flash-lite, minimal thinking7.5 s25.1 sSame amounts kept as 3.8-flash
3.1-flash-lite, low thinking7.0 s19.0 sMade no amount corrections
2.5-flash, no thinking14.3 s44.0 sCut one card page from 57 rows to 4

Two of those results are worth explaining. gemini-3.1-flash-lite was fast because it returned the input unchanged. It fixed none of the OCR errors, so as a page check it does nothing. And gemini-2.5-flash cut one credit card page from 57 rows to 4, which would have been lost data.

Accuracy, checked against the page

Speed does not matter if the model breaks the data, so we checked a one-page trial balance against the scanned page itself. Both gemini-3.8-flash and gemini-3.5-flash-lite repaired the same OCR damage, such as O0, .G0 and . 06 that should read .00, and blanked the repeated header row. The faster model sometimes wrote negative numbers in parentheses and merged one wrapped subtitle line. Our code converts parentheses to minus signs afterward, so the final output matched.

With the default cleanup instruction turned on

The converter now defaults to removing subtitles and other non-data rows. We replayed three documents (five pages) with that instruction set.

Page3.8-flash, low3.5-flash-lite, minimal
Ledger, 108 rows100 rows, 45 s100 rows, 19 s
Card statement A, page 166 to 12 rows, 16 s66 to 12 rows, 4 s
Card statement A, page 264 to 25 rows, 198 s64 to 25 rows, 6 s
Card statement B, page 12 to 3 rows2 to 3 rows
Card statement B, page 254 to 15 rows, clean54 to 35 rows, 16 junk rows left

The smaller model followed the instruction as well as the larger one on four of the five pages and was between 1.6 and 33 times faster. On the last page it left 16 rows without amounts that the larger model removed. The 198-second call is an outlier, but it matches the slow stretches we kept seeing.

What the thinking setting did

Gemini 3-series models take a thinking level. Higher levels let the model reason before answering, which costs time. We found two things.

  • gemini-3.8-flash rejects the minimal level with a 400 error. Low is the cheapest level it accepts. The lite models accept minimal.
  • For classification, more thinking did not help. We ran gemini-3.1-flash-lite at minimal, low, medium and high thinking on 20 documents, two runs each, and compared each answer to the route we expected (bank engine or column engine).
Thinking levelMean per callSlowest callWrong routes (of 40)
Minimal2.9 s5.3 s0
Low3.0 s4.7 s2
Medium3.6 s6.1 s1
High4.2 s7.8 s0

Minimal and high both routed every document as we expected, and high took about 45% longer. Low sent one credit card statement to the bank engine on both runs and medium on one. That is a small sample, so I would not read the difference between 0, 1 and 2 as a ranking. The finding is that nothing beat minimal.

Classifier comparison across models

We also compared three models on the classifier: the same 20 documents (13 bank statements, 7 column documents), two runs each, 40 calls per model. The expected route is our own judgment.

ModelMean per callSlowest callWrong routes (of 40)
3.8-flash, low thinking6.5 s37 s4
3.5-flash-lite, minimal2.0 s5.3 s2
3.1-flash-lite, minimal2.3 s5.6 s0

The larger model's misses included two runs that sent a scanned checking statement and one run that sent a scanned bank statement to the column engine, plus one run that returned the answer with literal quote marks around it, so our exact-match comparison would not have recognized it. That one is a code fix, described below.

The model assignments now

JobModelThinking
Classify the documentgemini-3.1-flash-liteMinimal
Bank statement page checkgemini-3.1-flash-liteMinimal
Column document page checkgemini-3.5-flash-liteMinimal
Find column headersgemini-2.5-flashModel default
Parse a hard line in the bank enginegemini-2.5-flash-liteModel default

We did not benchmark the last three. The bank page check already ran on a lite model and the other two are single short calls per document or only on lines the pattern rules cannot read. They are the next place to look if those calls show up in timings.

What we changed in the code

1. Tolerant parsing of the classifier's answer

The classifier now strips whitespace and surrounding quotes or backticks and lowercases the reply before comparing it to the expected labels. A correct answer that arrives as "bank_statement" used to be treated as an unknown type.

2. Check Tesseract's rotation guess first

Tesseract proposes a rotation and a confidence score. We still do not trust it blindly, because on sparse pages its direction is sometimes wrong. Before, we compared four readings of every flagged page. Now we compare the page as scanned with the page turned the way Tesseract suggested. If the suggested turn finds at least 1.5 times as many recognizable words and at least 8 of them, we accept it. Otherwise we try the other two turns exactly as before. A wrong turn reads as gibberish, so when the suggestion wins clearly, the other readings add nothing. The usual case drops from four low-resolution OCR passes to two.

3. One OCR pass per page

The column engine called two functions per scanned page. One returned word positions and the other returned plain text. Both rendered the same page at the same resolution, straightened it the same way and ran the same Tesseract call. The text was built from the word data, so the second run recomputed the first. Both now share one run: the plain text is built from the same word data using the same spacing code.

4. A per-page cache within one job

Results are kept for the 12 most recent pages, keyed by file path, size and modified time. Within one conversion, header detection, the main page loop and the fallback between engines reuse a page that was already read. The cache is emptied when a job ends. It does not carry anything from one customer's job to the next, and we decided against storing OCR text between jobs because it would mean keeping text from customer documents longer than a job.

How we checked that nothing changed

For 23 scanned pages from 12 documents, we recorded the rotation chosen, a hash of the OCR text, a hash of the word data, a hash of the line geometry and the image size, before and after the change.

CheckResult
Rotation, text, word data, geometry, image size on 23 pagesAll identical
Time for the same 23 pages (both OCR functions called per page)379 s before, 170 s after
Regression suite, 8 documentsSame document type, engine and page text on all 8
Unit tests27 of 27 pass

Splitting the 23 pages by whether they were sideways shows where the saving landed. Each page was read with both OCR functions, as the column engine does. Times are the average per page.

PagesCountBeforeAfter
Upright1912.8 s6.3 s
Sideways (turned 270 degrees)433.7 s12.7 s

Upright pages got about twice as fast, almost all of it from skipping the duplicate OCR pass. Sideways pages got about 2.7 times as fast because they also benefit from the shorter rotation check. The slowest page in the set, at 43 seconds before and 15 seconds after, was the first page of the first file in the run, so it includes some start-up cost. It is one page, so treat it as an outlier rather than a typical case.

The regression run showed row differences on two documents. On one, the row values were identical in all four reruns and only the AI's spelling of a column header varied. On the other, a cash app scan, the rows varied between runs of the old code too. It returned an empty result once in the regression and never in four reruns, and I could not reproduce it, so I cannot say what caused it.

What more CPU bought us

We run on Cloud Run with one request per container, 4 GiB of memory and 1 vCPU. A local test suggested extra CPU would barely help: with Tesseract capped at one thread, two pages took 15.6 seconds, and uncapped they took 13.2 seconds, about 15% faster. But that machine has 24 logical processors, so it says little about a container limited to one.

In production the picture was different. These are four-page previews, one run each.

Document1 CPU2 CPUs4 CPUs
13-page scanned credit card statement2:151:250:57
Cash app master scan1:050:400:47

The heavy scan got 2.4 times faster at 4 CPUs. The lighter file improved at 2 CPUs and did not at 4, which is within what I would expect from single runs. Fixed costs such as the AI calls do not shrink with more cores. Cloud Run bills by vCPU-seconds, so multiplying time by CPUs shows the compute cost per job.

Document1 CPU2 CPUs4 CPUs
13-page scanned credit card statement135 vCPU-s170 vCPU-s228 vCPU-s
Cash app master scan65 vCPU-s80 vCPU-s188 vCPU-s

Two CPUs cost about 25% more compute per job in exchange for about 37% less waiting on the heavy file. Four CPUs cost about 70% more for 58% less waiting. We are moving to 4 CPUs for now because the wait on large scans matters more to us than the extra cost, and we will revisit it as we collect real usage. Memory was not the limit: one page of OCR peaked at about 400 MB in a local measurement, which leaves room within 4 GiB.

What we did not do

  • OCR pages in parallel. The design is to OCR a few pages ahead of the parser with a small worker pool while the parsing code stays unchanged. It only pays off with more than one CPU, which we now have, so it is the next step. We want to re-time the two documents above with the OCR changes deployed before building it.
  • Run the AI page checks in parallel. In the column engine this is low risk once headers are known. In the bank engine the check for a page uses the statement year and month as they stood at the end of that page, which later pages can change, so checking every page afterward could restore the wrong year on a December-to-January statement. That needs a fix before it is safe.
  • Pay for higher thinking. The data above says it adds time without better routing.

Limits of these tests

  • The model benchmarks used 12 page checks and 20 classification documents. That is enough to rule out the clear losers, not to rank close results.
  • The expected route for each document is my judgment, and I mislabeled one file at first and corrected it after reading its contents.
  • Only the trial balance was checked cell by cell against the scanned page.
  • The timing results depend on the day. Gemini latency varied, with single calls as slow as 37 and 198 seconds.
  • The Cloud Run numbers are single runs with no repeat, and I did not record whether any hit a cold start.
  • I measured memory on Windows with only the OCR module loaded, not the full running app.

If you convert scanned statements or ledgers, you can try the converter with 50 free transactions and see how it handles your files. For the formats we support, see supported banks.

Jeff Gough

Written by

Jeff Gough

Founder of Statements to Sheets and Cascade Foundry LLC. Built this tool after converting 300+ pages of printed bank statements into CSV for a friend's QuickBooks catch-up. Read the full story →