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).
- 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_KEYis 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 taggedai_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.
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
pip install -r requirements.txtOCR 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.
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-genaiisn't installed - checkpip show google-genaiand re-runpip install -r requirements.txt.401 UNAUTHENTICATED/ACCESS_TOKEN_TYPE_UNSUPPORTEDon a key starting withAQ.- Google has been rolling out newerAQ.-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 everyAQ.key - regenerating the key from Google AI Studio fixed it in testing here.404 NOT_FOUND ... no longer available to new userson a dated model string likegemini-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 thegemini-flash-latestalias instead (the default above), which Google guarantees to keep pointed at a current model.
streamlit run app.pyUpload one or more bank statement PDFs in the sidebar and click Analyze statement(s).
python3 tests/test_pipeline.py # synthetic data only
python3 tests/test_pipeline.py /path/to/statement.pdf # also validates against a real PDF- 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\bnever 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.pytable-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).
generic_bank_parser.pyis 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 includingDD-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'sUPI/CR/.../UPI/DR/...and HDFC'sUPI-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\bword-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_KEYis 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.