mohsin-devs commited on
Commit
461d803
·
1 Parent(s): 1652384

Add OCR text extraction fallback with UI improvements

Browse files
Files changed (5) hide show
  1. .gitignore +5 -0
  2. app.py +65 -73
  3. packages.txt +2 -0
  4. requirements.txt +3 -0
  5. utils.py +65 -5
.gitignore CHANGED
@@ -10,3 +10,8 @@ src/
10
  .venv
11
  venv/
12
  env/
 
 
 
 
 
 
10
  .venv
11
  venv/
12
  env/
13
+
14
+ # Local OCR Windows Binaries
15
+ poppler-24.02.0/
16
+ poppler.zip
17
+ tesseract-setup.exe
app.py CHANGED
@@ -7,7 +7,6 @@ st.set_page_config(
7
  initial_sidebar_state="expanded"
8
  )
9
  import pandas as pd
10
- import numpy as np
11
  import plotly.express as px
12
  import time
13
  import json
@@ -592,8 +591,7 @@ def signup(username, email, password):
592
  if username in users:
593
  return False, "Username already exists"
594
 
595
- hashed = hash_password(password)
596
- persist_user(username, email, hashed)
597
  return True, "Account created successfully!"
598
 
599
  def logout():
@@ -605,24 +603,27 @@ def logout():
605
  st.session_state.current_chat_id = None
606
  clear_active_session()
607
 
608
- def get_mock_transactions():
609
- dates = pd.date_range(end=pd.Timestamp.now(), periods=30, freq='D')
610
-
611
- types = []
612
- amounts = []
613
- cats = []
614
- for _ in range(30):
615
- if np.random.random() > 0.8:
616
- types.append("Income")
617
- cats.append(np.random.choice(["Salary", "Investment", "Refund"]))
618
- amounts.append(round(float(np.random.uniform(5000, 25000)), 2))
619
- else:
620
- types.append("Expense")
621
- cats.append(np.random.choice(["Food", "Rent", "Shopping", "Transport", "Entertainment", "Utilities"]))
622
- amounts.append(round(float(np.random.uniform(100, 5000)), 2))
623
-
624
- data = {"Date": dates, "Category": cats, "Type": types, "Amount": amounts}
625
- return pd.DataFrame(data)
 
 
 
626
 
627
  def show_login_page():
628
  col1, col2, col3 = st.columns([1, 2, 1])
@@ -955,31 +956,34 @@ def show_dashboard():
955
 
956
  # Visualizations
957
  col_left, col_right = st.columns([2, 1])
958
- df = get_mock_transactions()
959
 
960
  with col_left:
961
  st.write("### 📉 Income vs Expenses")
962
- daily_data = df.groupby(['Date', 'Type'])['Amount'].sum().reset_index()
963
- fig_bar = px.bar(
964
- daily_data,
965
- x='Date',
966
- y='Amount',
967
- color='Type',
968
- barmode='group',
969
- color_discrete_map={"Income": st.session_state.colors['success'], "Expense": st.session_state.colors['danger']}
970
- )
971
- fig_bar.update_layout(margin=dict(t=0, b=0, l=0, r=0), height=300, paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", showlegend=True, legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1))
972
- if st.session_state.theme == "dark":
973
- fig_bar.update_layout(font_color="white")
974
  else:
975
- fig_bar.update_layout(font_color="black")
976
- st.plotly_chart(fig_bar, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
977
 
978
  with col_right:
979
  st.write("### 🍰 Expenses Breakdown")
980
  expense_df = df[df['Type'] == 'Expense']
981
  if expense_df.empty:
982
- st.info("No expenses recorded yet.")
983
  else:
984
  category_data = expense_df.groupby('Category')['Amount'].sum().reset_index()
985
  fig = px.pie(
@@ -1007,11 +1011,17 @@ def show_dashboard():
1007
 
1008
  # Consolidated Transactions
1009
  st.markdown("### 📝 Recent Transaction History")
1010
- st.dataframe(
1011
- df.sort_values(by="Date", ascending=False),
1012
- use_container_width=True,
1013
- hide_index=True
1014
- )
 
 
 
 
 
 
1015
 
1016
  elif page == "Banking Assistant":
1017
  is_connected = check_ollama_connection()
@@ -1062,53 +1072,41 @@ def show_dashboard():
1062
  if uploaded_file:
1063
  if st.button("Analyze Statement", type="primary"):
1064
  with st.spinner(t("analyzing")):
1065
- text = extract_text_from_pdf(uploaded_file)
1066
  if text:
1067
  st.session_state.faq_trigger = "I have uploaded a bank statement. Please summarize it: " + text[:1500]
 
1068
  else:
1069
- st.error("Failed to extract text from PDF.")
1070
 
1071
  # 🎙️ Voice Support UI
1072
- st.markdown("<div style='margin-bottom: 20px;'></div>", unsafe_allow_html=True)
1073
- voice_col1, voice_col2 = st.columns([1, 1])
1074
- with voice_col1:
1075
- if st.button("🎤 Start Voice Input"):
1076
- st.components.v1.html("""
1077
- <script>
1078
- const recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)();
1079
- recognition.lang = 'en-US';
1080
- recognition.start();
1081
- recognition.onresult = (event) => {
1082
- const text = event.results[0][0].transcript;
1083
- window.parent.postMessage({type: 'voice_input', text: text}, '*');
1084
- };
1085
- </script>
1086
- """, height=0)
1087
- st.info("Listening... Please speak now.")
1088
- with voice_col2:
1089
- st.session_state.tts_enabled = st.toggle("🔊 Voice Responses (TTS)", value=st.session_state.get("tts_enabled", False))
1090
 
1091
  chat_container = st.container(height=400, border=False)
1092
 
1093
  with chat_container:
1094
  for message in st.session_state.messages:
1095
  role = message["role"]
 
1096
  if role == "user":
1097
- st.markdown(f'<div class="user-bubble">{message["content"]}</div>', unsafe_allow_html=True)
1098
  else:
1099
- st.markdown(f'<div class="ai-bubble">{message["content"]}</div>', unsafe_allow_html=True)
1100
 
1101
  prompt = st.chat_input(t("chat_input"))
1102
 
 
1103
  if getattr(st.session_state, 'faq_trigger', None):
1104
  prompt = st.session_state.faq_trigger
 
1105
  st.session_state.faq_trigger = None
 
1106
 
1107
  if prompt:
1108
- st.session_state.messages.append({"role": "user", "content": prompt})
1109
 
1110
  with chat_container:
1111
- st.markdown(f'<div class="user-bubble">{prompt}</div>', unsafe_allow_html=True)
1112
 
1113
  faq_response = get_faq_response(prompt, language=st.session_state.get("language", "English"))
1114
 
@@ -1144,13 +1142,7 @@ def show_dashboard():
1144
  st.session_state.messages.append({"role": "assistant", "content": full_response})
1145
 
1146
  # 🔊 Handle Text-to-Speech
1147
- if st.session_state.get("tts_enabled", False):
1148
- st.components.v1.html(f"""
1149
- <script>
1150
- const msg = new SpeechSynthesisUtterance({json.dumps(full_response)});
1151
- window.speechSynthesis.speak(msg);
1152
- </script>
1153
- """, height=0)
1154
  # Save using the persistent utility
1155
  new_id = save_chat_session(st.session_state.username, st.session_state, st.session_state.messages, st.session_state.current_chat_id)
1156
  if not st.session_state.current_chat_id:
 
7
  initial_sidebar_state="expanded"
8
  )
9
  import pandas as pd
 
10
  import plotly.express as px
11
  import time
12
  import json
 
591
  if username in users:
592
  return False, "Username already exists"
593
 
594
+ persist_user(username, email, password)
 
595
  return True, "Account created successfully!"
596
 
597
  def logout():
 
603
  st.session_state.current_chat_id = None
604
  clear_active_session()
605
 
606
+ def get_user_transactions_df(username):
607
+ """Builds a dashboard-friendly DataFrame from stored user transactions."""
608
+ transactions = get_transactions(username)
609
+ if not transactions:
610
+ return pd.DataFrame(columns=["Date", "Category", "Type", "Amount", "Details", "Direction"])
611
+
612
+ rows = []
613
+ for txn in transactions:
614
+ raw_type = str(txn.get("type", "")).lower()
615
+ rows.append({
616
+ "Date": pd.to_datetime(txn.get("date"), errors="coerce"),
617
+ "Category": txn.get("category", "Other") or "Other",
618
+ "Type": "Income" if raw_type == "credit" else "Expense",
619
+ "Amount": float(txn.get("amount", 0) or 0),
620
+ "Details": txn.get("details", ""),
621
+ "Direction": raw_type.title() if raw_type else "Unknown"
622
+ })
623
+
624
+ df = pd.DataFrame(rows)
625
+ df["Date"] = df["Date"].fillna(pd.Timestamp.now())
626
+ return df.sort_values(by="Date", ascending=False).reset_index(drop=True)
627
 
628
  def show_login_page():
629
  col1, col2, col3 = st.columns([1, 2, 1])
 
956
 
957
  # Visualizations
958
  col_left, col_right = st.columns([2, 1])
959
+ df = get_user_transactions_df(st.session_state.username)
960
 
961
  with col_left:
962
  st.write("### 📉 Income vs Expenses")
963
+ if df.empty:
964
+ st.info("No transactions yet. Make a transfer or add account activity to see your trends.")
 
 
 
 
 
 
 
 
 
 
965
  else:
966
+ daily_data = df.groupby([pd.Grouper(key="Date", freq="D"), "Type"])["Amount"].sum().reset_index()
967
+ fig_bar = px.bar(
968
+ daily_data,
969
+ x='Date',
970
+ y='Amount',
971
+ color='Type',
972
+ barmode='group',
973
+ color_discrete_map={"Income": st.session_state.colors['success'], "Expense": st.session_state.colors['danger']}
974
+ )
975
+ fig_bar.update_layout(margin=dict(t=0, b=0, l=0, r=0), height=300, paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", showlegend=True, legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1))
976
+ if st.session_state.theme == "dark":
977
+ fig_bar.update_layout(font_color="white")
978
+ else:
979
+ fig_bar.update_layout(font_color="black")
980
+ st.plotly_chart(fig_bar, use_container_width=True)
981
 
982
  with col_right:
983
  st.write("### 🍰 Expenses Breakdown")
984
  expense_df = df[df['Type'] == 'Expense']
985
  if expense_df.empty:
986
+ st.info("No expense transactions recorded yet.")
987
  else:
988
  category_data = expense_df.groupby('Category')['Amount'].sum().reset_index()
989
  fig = px.pie(
 
1011
 
1012
  # Consolidated Transactions
1013
  st.markdown("### 📝 Recent Transaction History")
1014
+ if df.empty:
1015
+ st.info("Your transaction history will appear here after your first account activity.")
1016
+ else:
1017
+ history_df = df.copy()
1018
+ history_df["Date"] = history_df["Date"].dt.strftime("%Y-%m-%d %H:%M:%S")
1019
+ history_df["Amount"] = history_df["Amount"].map(format_currency)
1020
+ st.dataframe(
1021
+ history_df[["Date", "Direction", "Type", "Category", "Amount", "Details"]],
1022
+ use_container_width=True,
1023
+ hide_index=True
1024
+ )
1025
 
1026
  elif page == "Banking Assistant":
1027
  is_connected = check_ollama_connection()
 
1072
  if uploaded_file:
1073
  if st.button("Analyze Statement", type="primary"):
1074
  with st.spinner(t("analyzing")):
1075
+ text, error = extract_text_from_pdf(uploaded_file)
1076
  if text:
1077
  st.session_state.faq_trigger = "I have uploaded a bank statement. Please summarize it: " + text[:1500]
1078
+ st.session_state.faq_display = "I have uploaded a bank statement. Please summarize it."
1079
  else:
1080
+ st.error(f"Failed to extract text from PDF: {error}")
1081
 
1082
  # 🎙️ Voice Support UI
1083
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1084
 
1085
  chat_container = st.container(height=400, border=False)
1086
 
1087
  with chat_container:
1088
  for message in st.session_state.messages:
1089
  role = message["role"]
1090
+ display_content = message.get("display_content", message["content"])
1091
  if role == "user":
1092
+ st.markdown(f'<div class="user-bubble">{display_content}</div>', unsafe_allow_html=True)
1093
  else:
1094
+ st.markdown(f'<div class="ai-bubble">{display_content}</div>', unsafe_allow_html=True)
1095
 
1096
  prompt = st.chat_input(t("chat_input"))
1097
 
1098
+ display_prompt = prompt
1099
  if getattr(st.session_state, 'faq_trigger', None):
1100
  prompt = st.session_state.faq_trigger
1101
+ display_prompt = getattr(st.session_state, 'faq_display', prompt)
1102
  st.session_state.faq_trigger = None
1103
+ st.session_state.faq_display = None
1104
 
1105
  if prompt:
1106
+ st.session_state.messages.append({"role": "user", "content": prompt, "display_content": display_prompt})
1107
 
1108
  with chat_container:
1109
+ st.markdown(f'<div class="user-bubble">{display_prompt}</div>', unsafe_allow_html=True)
1110
 
1111
  faq_response = get_faq_response(prompt, language=st.session_state.get("language", "English"))
1112
 
 
1142
  st.session_state.messages.append({"role": "assistant", "content": full_response})
1143
 
1144
  # 🔊 Handle Text-to-Speech
1145
+ # Voice input and TTS removed
 
 
 
 
 
 
1146
  # Save using the persistent utility
1147
  new_id = save_chat_session(st.session_state.username, st.session_state, st.session_state.messages, st.session_state.current_chat_id)
1148
  if not st.session_state.current_chat_id:
packages.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ tesseract-ocr
2
+ poppler-utils
requirements.txt CHANGED
@@ -7,3 +7,6 @@ groq
7
  pillow
8
  watchdog
9
  PyPDF2
 
 
 
 
7
  pillow
8
  watchdog
9
  PyPDF2
10
+ pdf2image
11
+ pytesseract
12
+ opencv-python-headless
utils.py CHANGED
@@ -243,17 +243,77 @@ def save_intents(data):
243
  print(f"Error saving intents: {e}")
244
  return False
245
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  def extract_text_from_pdf(pdf_file):
247
- """Extracts text from an uploaded PDF file."""
248
  try:
 
 
249
  reader = PyPDF2.PdfReader(pdf_file)
250
  text = ""
251
  for page in reader.pages:
252
- text += page.extract_text()
253
- return text
 
 
 
 
 
 
 
 
254
  except Exception as e:
255
- print(f"Error extracting PDF text: {e}")
256
- return None
 
 
257
 
258
  def clear_active_session():
259
  if os.path.exists(SESSION_FILE):
 
243
  print(f"Error saving intents: {e}")
244
  return False
245
 
246
+ def extract_text_with_ocr(pdf_file):
247
+ """Fallback OCR extraction for scanned or image-based PDFs."""
248
+ try:
249
+ import pytesseract
250
+ import cv2
251
+ import numpy as np
252
+ from pdf2image import convert_from_bytes
253
+ import os
254
+ import platform
255
+
256
+ if platform.system() == 'Windows':
257
+ # Hardcode path for local Windows testing
258
+ pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
259
+ poppler_path = os.path.join(os.path.dirname(__file__), 'poppler-24.02.0', 'Library', 'bin')
260
+ else:
261
+ poppler_path = None
262
+ except ImportError as e:
263
+ raise Exception(f"OCR Python packages missing: {e}. Please install pdf2image, pytesseract, opencv-python-headless, numpy.")
264
+
265
+ try:
266
+ if hasattr(pdf_file, 'seek'):
267
+ pdf_file.seek(0)
268
+
269
+ pdf_bytes = pdf_file.read()
270
+ if platform.system() == 'Windows':
271
+ images = convert_from_bytes(pdf_bytes, poppler_path=poppler_path)
272
+ else:
273
+ images = convert_from_bytes(pdf_bytes)
274
+
275
+ text = ""
276
+ for img in images:
277
+ img_cv = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
278
+ gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)
279
+ thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)[1]
280
+
281
+ page_text = pytesseract.image_to_string(thresh)
282
+ text += page_text + "\n"
283
+
284
+ text = text.replace('₹', 'Rs.')
285
+ text = re.sub(r'\n+', '\n', text)
286
+
287
+ extracted = text.strip()
288
+ if not extracted:
289
+ raise Exception("OCR completed but no text was found in the images.")
290
+ return extracted
291
+ except Exception as e:
292
+ raise Exception(f"OCR System dependencies missing or failed: {e}. Make sure Tesseract OCR and Poppler are installed on your OS and added to PATH.")
293
+
294
  def extract_text_from_pdf(pdf_file):
295
+ """Extracts text from an uploaded PDF file with OCR fallback. Returns (text, error)."""
296
  try:
297
+ if hasattr(pdf_file, 'seek'):
298
+ pdf_file.seek(0)
299
  reader = PyPDF2.PdfReader(pdf_file)
300
  text = ""
301
  for page in reader.pages:
302
+ page_text = page.extract_text()
303
+ if page_text:
304
+ text += page_text
305
+
306
+ extracted = text.strip()
307
+ if extracted:
308
+ return extracted, None
309
+
310
+ # Fallback to OCR if empty
311
+ return extract_text_with_ocr(pdf_file), None
312
  except Exception as e:
313
+ try:
314
+ return extract_text_with_ocr(pdf_file), None
315
+ except Exception as ocr_error:
316
+ return None, str(ocr_error)
317
 
318
  def clear_active_session():
319
  if os.path.exists(SESSION_FILE):