Skip to content

About

Template-free bank statement extraction and analytics — PDF + OCR parsing, duplicate/balance validation with per-txn confidence scores, rule-based categorization, Streamlit dashboard. Optional Gemini query layer.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Bank Statement Intelligence Platform

A template-free bank statement extraction, validation, and analytics engine, built in Python. Handles digital and scanned PDFs, currently tuned for UPI-wallet statements (PhonePe / Paytm / GPay style) and a best-effort generic parser for traditional tabular bank statements (Date | Description | Debit | Credit | Balance).

What it does

  • Extraction - reads digital PDF text directly (pdfplumber); automatically OCRs any page that has no usable text layer (scanned/image pages), via Tesseract + pdf2image. No manual template per bank.
  • Format detection - auto-detects UPI-wallet-style free-text statements vs. traditional tabular bank statements and routes to the matching parser.
  • Validation - flags duplicate transactions (matching transaction ID / UTR), transactions outside the stated statement period, running-balance mismatches (for statements that carry a balance column), amount outliers, and assigns a 0-1 confidence score per transaction and a 0-100 integrity score per statement.
  • Categorization - rule-based keyword categorization (groceries, food & dining, fuel, bills, shopping, investments, entertainment, etc.) - no external API calls.
  • Analytics - spend/income summary, category breakdown, daily cash flow, top merchants, recurring payment/subscription detection, a financial health score, a naive 30-day cash-flow projection, and rule-based budgeting recommendations.
  • Dashboard - Streamlit app with filterable transaction table, charts, validation report, analytics, CSV/Excel export, and a natural-language query box.
  • Storage - SQLite persistence so multiple statements can be uploaded over time and browsed together.
  • AI layer (optional) - if a GEMINI_API_KEY is configured, the query box and recommendations are answered by Gemini instead of the rule-based engine, with real-time fallback to the rule-based engine if the key/package/network isn't available. The app works fully without this configured at all. Additionally, if a statement's layout doesn't match any known bank template - all rule-based parsers return nothing, or return mostly unknown-direction / low-confidence rows - and Gemini is configured, the raw statement text is sent to Gemini as a last-resort extraction fallback (core/ai/ai_extraction.py). Rows extracted this way are tagged ai_extracted, start at a lower confidence score, and the Validation tab shows a warning banner so they're clearly distinguishable from coordinate/table-based extraction. If this also fails or isn't configured, the app falls back to whatever the rule-based parsers produced.

Project layout

bsi/
  app.py                        Streamlit dashboard (entry point)
  core/
    models.py                   Transaction / StatementMeta / ValidationSummary dataclasses
    pipeline.py                 End-to-end orchestration (PDF -> insights)
    extraction/pdf_extractor.py PDF text extraction + OCR fallback
    parsing/
      format_detector.py        UPI-wallet vs tabular bank statement detection (coarse fallback heuristic)
      upi_wallet_parser.py      Block-regex parser for PhonePe/UPI-style statements
      tabular_wrapped_parser.py Coordinate-based parser: Txn Date|Value Date|Particulars|Ref No|Debit|Credit|Balance
      hdfc_parser.py            Coordinate-based parser: Date|Narration|Chq./Ref.No.|ValueDt|Withdrawal|Deposit|Balance
      generic_bank_parser.py    Table-extraction + heuristic parser for any other tabular bank statement
    validation/validators.py    Duplicate/continuity/balance/confidence checks
    categorization/             Rule-based transaction categorization
    analytics/analytics_engine.py  Summaries, trends, recurring payments, health score, forecast
    nlq/query_engine.py         Rule-based natural-language query engine
    ai/gemini_client.py         Optional Gemini API wrapper (graceful no-op if unconfigured)
    ai/insights.py              AI-powered query answering + recommendations, with rule-based fallback
    storage/db.py               SQLite persistence
  tests/test_pipeline.py        Smoke tests (synthetic + optional real PDF)
  .env                          GEMINI_API_KEY / GEMINI_MODEL (optional)
  requirements.txt

Setup

pip install -r requirements.txt

OCR fallback needs Tesseract and Poppler installed on your system (only used for scanned/image pages):

  • macOS: brew install tesseract poppler
  • Ubuntu/Debian: sudo apt install tesseract-ocr poppler-utils
  • Windows: install Tesseract and Poppler binaries and ensure they're on PATH.

Optional: AI-powered query box and recommendations

A .env file at the project root controls this:

GEMINI_API_KEY=your-key-here
GEMINI_MODEL=gemini-flash-latest

Leave GEMINI_API_KEY blank or delete the file to run fully offline on the rule-based engine - nothing else in the app depends on it. gemini-flash-latest is used as the default model rather than a dated model string (e.g. gemini-2.5-flash-lite) because Google periodically retires dated model IDs for new API keys/projects - -latest is a rolling alias Google keeps pointed at a current, available Flash model.

If AI answers/recommendations silently fall back to the rule-based engine, the "Ask your data" tab and the "Generate AI-powered insights" button show the actual failure reason next to "AI call failed" instead of failing silently - check that message first when diagnosing why AI isn't being used. Known causes seen so far:

  • google-genai isn't installed - check pip show google-genai and re-run pip install -r requirements.txt.
  • 401 UNAUTHENTICATED / ACCESS_TOKEN_TYPE_UNSUPPORTED on a key starting with AQ. - Google has been rolling out newer AQ.-prefixed keys (see the Google AI Developer forum for others hitting this), and some of these keys get rejected outright. In practice this looks intermittent/key-specific rather than affecting every AQ. key - regenerating the key from Google AI Studio fixed it in testing here.
  • 404 NOT_FOUND ... no longer available to new users on a dated model string like gemini-2.5-flash-lite - that specific model has been cut off for newer API keys/projects even though it's still listed in Google's docs. Fixed by using the gemini-flash-latest alias instead (the default above), which Google guarantees to keep pointed at a current model.

Run the dashboard

streamlit run app.py

Upload one or more bank statement PDFs in the sidebar and click Analyze statement(s).

Run tests

python3 tests/test_pipeline.py                       # synthetic data only
python3 tests/test_pipeline.py /path/to/statement.pdf # also validates against a real PDF

Validated against

  • A real 15-page PhonePe UPI transaction statement (125 transactions, digital text). Extraction was independently cross-checked against a coordinate-based re-parse of the same PDF and matched exactly (108 debits totaling ₹327,197.94, 17 credits totaling ₹154,537.00). Handles amounts that wrap onto the following visual line for large values, "Paid - Mobile Recharge" entries with no named counterparty, and bare "Paid" entries with no recipient at all.
  • A real 8-page traditional Indian bank statement (139 transactions, "Txn Date | Value Date | Particulars | Ref. No. | Debit | Credit | Balance" wrapped-column layout, handled by parsing/tabular_wrapped_parser.py). The reconstructed running balance lands exactly on the statement's own stated "Current Balance," and all balance-continuity checks pass with 0 issues - a full end-to-end reconciliation against the source document.
  • A real 14-page HDFC Bank statement (134 transactions, "Date | Narration | Chq./Ref.No. | ValueDt | WithdrawalAmt. | DepositAmt. | ClosingBalance" layout, handled by parsing/hdfc_parser.py). This PDF's text layer also omits spaces within wrapped narration lines (e.g. "PAYMENTFROMPHONE"), which broke the coarse format-detection heuristic entirely on first pass (word-boundary regexes like \bBalance\b never matched "ClosingBalance") - format detection was restructured so header-signature-based parser checks run before the coarse heuristic. Extraction matches the statement's own printed "STATEMENT SUMMARY" totals exactly: 110 debits = ₹248,278.94, 24 credits = ₹246,274.00, closing balance = ₹17,128.64.
  • A real 4-page PNB (Punjab National Bank) statement (35 transactions, "Date | Amount | Type | Instrument No | Balance | Remarks" layout with a real PDF-native table grid, handled by the hardened generic_bank_parser.py table-extraction path). This one has a single unsigned Amount column plus a separate CREDIT/DEBIT "Type" column rather than split debit/credit columns - direction detection now reads that column instead of guessing from the amount's sign. It also exposed a real bug: pdfplumber returns one independent table per page, and the column header only prints on page 1 - the parser previously required every table to carry its own header row, so pages 2-4 (whose tables start straight with data, no header) were silently discarded entirely, extracting only 2 of 35 transactions. Fixed by determining the column mapping once from whichever table in the document has a real header, then applying it to every table's rows. Reconstructed running balance matches the statement exactly across all 35 transactions with 0 mismatches (₹218,628.00 total debits, ₹216,134.00 total credits).

Known limitations / next steps

  • generic_bank_parser.py is a generic fallback for tabular bank statements that don't match the header-signature-based dedicated parsers below. It's been hardened using researched Indian bank conventions (broader date-format matching including DD-MMM-YYYY, a wider header-keyword vocabulary - value date, txn date, chq/ref no, cheque no, dr/cr amt, remarks, type, etc. - direction detection that recognizes UPI narration prefixes like SBI's UPI/CR/.../UPI/DR/... and HDFC's UPI-CREDIT-.../UPI-DEBIT-... style, and a single-amount-plus-type-column layout), and is now validated against one real PDF (the PNB statement above) in addition to the three dedicated-parser formats - industry sources indicate production tools maintain 30+ dedicated per-bank parsers because column phrasing and layout genuinely differ bank to bank, so other banks routed through this fallback are still best-effort until confirmed against a real statement. The dedicated parsers (upi_wallet_parser.py, tabular_wrapped_parser.py, hdfc_parser.py) each handle one specific real-world template precisely via word-coordinate reconstruction; a genuinely different layout would need a similar dedicated parser - send a sample PDF and it can be added and validated the same way.
  • format_detector.py's tabular-format keyword matching no longer uses \b word-boundary regexes, since some real statement PDFs (confirmed on the HDFC sample) drop spaces between words in the text layer (e.g. "ClosingBalance"), which silently breaks word-boundary matching.
  • The categorization ruleset is a starting set of keyword rules - easy to extend in core/categorization/rules.py.
  • The NLQ box uses Gemini when GEMINI_API_KEY is configured (open-ended questions work); without it, it falls back to a pattern-based engine recognizing a fixed set of question shapes (see the "Ask your data" tab for examples).
  • Fraud/tamper detection is currently limited to duplicate detection, balance-continuity checks, and amount-outlier flags - a production system would add more (e.g. font/layout consistency checks on the raw PDF, cross-statement reconciliation).
  • No REST API / multi-user auth layer yet - this is the core engine + a single-user local dashboard, per the current build scope.

About

Template-free bank statement extraction and analytics — PDF + OCR parsing, duplicate/balance validation with per-txn confidence scores, rule-based categorization, Streamlit dashboard. Optional Gemini query layer.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages