Spaces:
Running
Running
Commit ·
461d803
1
Parent(s): 1652384
Add OCR text extraction fallback with UI improvements
Browse files- .gitignore +5 -0
- app.py +65 -73
- packages.txt +2 -0
- requirements.txt +3 -0
- 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 |
-
|
| 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
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 959 |
|
| 960 |
with col_left:
|
| 961 |
st.write("### 📉 Income vs Expenses")
|
| 962 |
-
|
| 963 |
-
|
| 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 |
-
|
| 976 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 1011 |
-
|
| 1012 |
-
|
| 1013 |
-
|
| 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 |
-
|
| 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">{
|
| 1098 |
else:
|
| 1099 |
-
st.markdown(f'<div class="ai-bubble">{
|
| 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">{
|
| 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 |
-
|
| 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 |
-
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
except Exception as e:
|
| 255 |
-
|
| 256 |
-
|
|
|
|
|
|
|
| 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):
|