|
import streamlit as st |
|
import speech_recognition as sr |
|
import requests |
|
from twilio.rest import Client |
|
import re |
|
from word2number import w2n |
|
import os |
|
import datetime |
|
import json |
|
from dotenv import load_dotenv |
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
TWILIO_SID = os.getenv('TWILIO_SID') |
|
TWILIO_AUTH_TOKEN = os.getenv('TWILIO_AUTH_TOKEN') |
|
TWILIO_PHONE_NUMBER = os.getenv('TWILIO_PHONE_NUMBER') |
|
|
|
|
|
INVOICE_GEN_API_URL = os.getenv('INVOICE_GEN_API_URL') |
|
INVOICE_GEN_API_KEY = os.getenv('INVOICE_GEN_API_KEY') |
|
|
|
|
|
GEMINI_API_KEY = os.getenv('GEMINI_API_KEY') |
|
|
|
|
|
|
|
|
|
recognizer = sr.Recognizer() |
|
|
|
|
|
if "is_listening" not in st.session_state: |
|
st.session_state.is_listening = False |
|
if "customer_name" not in st.session_state: |
|
st.session_state.customer_name = "" |
|
if "customer_number" not in st.session_state: |
|
st.session_state.customer_number = "" |
|
if "bill_content" not in st.session_state: |
|
st.session_state.bill_content = "" |
|
if "currency" not in st.session_state: |
|
st.session_state.currency = "USD" |
|
|
|
|
|
language_map = { |
|
'English': 'en-US', |
|
'Urdu': 'ur' |
|
} |
|
|
|
|
|
def recognize_speech(language_code): |
|
"""Listen to the microphone and return the recognized text in the selected language""" |
|
with sr.Microphone() as source: |
|
st.write("Listening...") |
|
audio = recognizer.listen(source) |
|
try: |
|
text = recognizer.recognize_google(audio, language=language_code) |
|
st.write(f"You said: {text}") |
|
return text |
|
except Exception as e: |
|
st.write("Sorry, I couldn't understand the audio.") |
|
return None |
|
|
|
|
|
def toggle_listen(button_key): |
|
"""Toggle listening on and off""" |
|
st.session_state.is_listening = not st.session_state.is_listening |
|
|
|
|
|
|
|
def convert_number_words_to_digits(text, language): |
|
"""Converts numbers written in words to digits in the text while preserving leading zeros.""" |
|
if language == 'English': |
|
|
|
stripped_text = ''.join(c for c in text if c.isdigit() or c.isspace() or c in '+-') |
|
if stripped_text and all(c.isdigit() or c.isspace() or c in '+-' for c in text): |
|
|
|
return text |
|
|
|
|
|
words = text.split() |
|
for i, word in enumerate(words): |
|
try: |
|
|
|
num = w2n.word_to_num(word) |
|
words[i] = str(num) |
|
except ValueError: |
|
|
|
continue |
|
return ' '.join(words) |
|
elif language == 'Urdu': |
|
|
|
if re.match(r'^[\d\s\+\-]+$', text): |
|
return text |
|
|
|
|
|
persian_numbers = { |
|
"ایک": "1", "دو": "2", "تین": "3", "چار": "4", "پانچ": "5", |
|
"چھے": "6", "سات": "7", "آٹھ": "8", "نو": "9", "دس": "10", |
|
"گیارہ": "11", "بارہ": "12", "تیرہ": "13", "چودہ": "14", |
|
"پندرہ": "15", "سولہ": "16", "سترہ": "17", "اٹھارہ": "18", |
|
"انیس": "19", "بیس": "20", |
|
"تیس": "30", "چالیس": "40", "پچاس": "50", "ساٹھ": "60", |
|
"ستر": "70", "اسّی": "80", "نوے": "90", "سو": "100", |
|
"صفر": "0" |
|
} |
|
|
|
for word, digit in persian_numbers.items(): |
|
text = re.sub(r'\b' + word + r'\b', digit, text, flags=re.IGNORECASE) |
|
return text |
|
else: |
|
return text |
|
|
|
|
|
|
|
|
|
def extract_item_details_from_gemini(bill_content): |
|
"""Extract structured item details from Gemini API.""" |
|
prompt = f"Extract structured JSON for bill items, including item names, quantities, and prices from the following text: '{bill_content}'" |
|
|
|
|
|
gemini_api_url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={GEMINI_API_KEY}" |
|
|
|
headers = { |
|
"Content-Type": "application/json" |
|
} |
|
|
|
payload = { |
|
"contents": [{ |
|
"parts": [{"text": prompt}] |
|
}] |
|
} |
|
|
|
try: |
|
response = requests.post(gemini_api_url, json=payload, headers=headers) |
|
response.raise_for_status() |
|
data = response.json() |
|
|
|
|
|
|
|
|
|
|
|
|
|
raw_json = data['candidates'][0]['content']['parts'][0]['text'] |
|
|
|
|
|
cleaned_json = raw_json.strip('```json').strip().strip('```').strip() |
|
|
|
|
|
try: |
|
structured_items = json.loads(cleaned_json) |
|
return structured_items |
|
except json.JSONDecodeError as e: |
|
st.write(f"Error parsing JSON: {e}") |
|
return None |
|
|
|
except requests.exceptions.RequestException as e: |
|
st.write(f"Error extracting item details: {e}") |
|
return None |
|
|
|
|
|
def generate_invoice_pdf(customer_name, customer_number, items, currency): |
|
"""Generate invoice PDF using Invoice Generator API.""" |
|
|
|
current_date = datetime.datetime.now().strftime("%b %d, %Y") |
|
due_date = (datetime.datetime.now() + datetime.timedelta(days=7)).strftime("%b %d, %Y") |
|
invoice_number = f"INV-{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}" |
|
|
|
data = { |
|
"from": "Saqib Zeen House (Textile)", |
|
"to": customer_name, |
|
"logo": "https://example.com/logo.png", |
|
"number": invoice_number, |
|
"date": current_date, |
|
"due_date": due_date, |
|
"currency": currency, |
|
"notes": """● Thank you for your business! We appreciate your trust and look forward to serving you again. |
|
● Great choice! Quality products, fair pricing, and timely service—what more could you ask for? |
|
● If this invoice were a novel, the ending would be "Paid in Full." Let's make it a bestseller! |
|
● Questions? Concerns? Compliments? We're just a message away.""", |
|
"terms": """● Payment is due by the due date to keep our accountants happy (and to avoid late fees). |
|
● Late payments may result in a [X]% charge per month—or worse, a strongly worded email. |
|
● If you notice any discrepancies, please inform us within [X] days. We promise we didn't do it on purpose. |
|
● We accept payments via [Bank Transfer, PayPal, Credit Card, etc.]. Choose wisely, but choose soon.""" |
|
} |
|
|
|
|
|
for i, item in enumerate(items): |
|
data[f"items[{i}][name]"] = item["item_name"] |
|
data[f"items[{i}][quantity]"] = item["quantity"] |
|
|
|
|
|
if "price" in item: |
|
data[f"items[{i}][unit_cost]"] = item["price"] |
|
elif "price_per_item" in item: |
|
data[f"items[{i}][unit_cost]"] = item["price_per_item"] |
|
else: |
|
|
|
st.write("Warning: Price field not found in item data") |
|
data[f"items[{i}][unit_cost]"] = 0 |
|
|
|
headers = { |
|
'Authorization': f'Bearer {INVOICE_GEN_API_KEY}', |
|
'Content-Type': 'application/x-www-form-urlencoded' |
|
} |
|
|
|
try: |
|
response = requests.post(INVOICE_GEN_API_URL, headers=headers, data=data) |
|
response.raise_for_status() |
|
|
|
with open("invoice.pdf", "wb") as f: |
|
f.write(response.content) |
|
|
|
return "invoice.pdf" |
|
except requests.exceptions.RequestException as e: |
|
st.write(f"Error generating invoice: {e}") |
|
return None |
|
|
|
|
|
def upload_to_tempfiles(file_path): |
|
"""Uploads the file to tempfiles.org and returns the public link.""" |
|
url = "https://tmpfiles.org/api/v1/upload" |
|
|
|
|
|
with open(file_path, 'rb') as file: |
|
files = {'file': file} |
|
try: |
|
response = requests.post(url, files=files) |
|
response.raise_for_status() |
|
|
|
|
|
response_json = response.json() |
|
if response_json.get("status") == "success": |
|
file_url = response_json["data"].get("url") |
|
|
|
if file_url and "tmpfiles.org" in file_url: |
|
file_url = file_url.replace("tmpfiles.org/", "tmpfiles.org/dl/") |
|
return file_url |
|
else: |
|
st.write(f"Error: {response_json.get('status')}") |
|
return None |
|
except requests.exceptions.RequestException as e: |
|
st.write(f"Error uploading file: {e}") |
|
return None |
|
|
|
|
|
|
|
def send_pdf_via_whatsapp(pdf_file, customer_number): |
|
"""Uploads the PDF to tempfiles.org and sends the generated PDF to the customer via WhatsApp.""" |
|
|
|
pdf_file_url = upload_to_tempfiles(pdf_file) |
|
st.write(pdf_file_url) |
|
|
|
if not pdf_file_url: |
|
return "Error: Could not upload file to tempfiles.org." |
|
|
|
client = Client(TWILIO_SID, TWILIO_AUTH_TOKEN) |
|
|
|
try: |
|
message = client.messages.create( |
|
body="Boss, this bill is honored to be in your inbox. Now, do it a favor and make it disappear.", |
|
from_=TWILIO_PHONE_NUMBER, |
|
media_url=[pdf_file_url], |
|
to=f'whatsapp:{customer_number}' |
|
) |
|
return f"Bill successfully sent to {customer_number}" |
|
except Exception as e: |
|
return f"Error sending bill via WhatsApp: {str(e)}" |
|
|
|
|
|
|
|
|
|
|
|
st.markdown( |
|
""" |
|
<style> |
|
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); |
|
|
|
:root { |
|
--dark-green: #2C3930; |
|
--medium-green: #3F4F44; |
|
--accent-brown: #A27B5C; |
|
--light-cream: #DCD7C9; |
|
} |
|
|
|
body { |
|
font-family: 'Poppins', sans-serif; |
|
background-color: var(--dark-green); |
|
color: var(--light-cream); |
|
} |
|
|
|
/* Main container styling */ |
|
.stApp { |
|
background: linear-gradient(135deg, var(--dark-green) 0%, #263228 100%); |
|
} |
|
|
|
/* Header styling */ |
|
.title { |
|
font-family: 'Poppins', sans-serif; |
|
text-align: center; |
|
font-size: 3.5rem; |
|
font-weight: 700; |
|
background: linear-gradient(90deg, var(--accent-brown), var(--light-cream) 70%); |
|
-webkit-background-clip: text; |
|
-webkit-text-fill-color: transparent; |
|
text-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); |
|
letter-spacing: 1px; |
|
margin-top: 1.5rem; |
|
margin-bottom: 0.5rem; |
|
transform: scale(1); |
|
transition: transform 0.3s ease-in-out; |
|
} |
|
|
|
.title:hover { |
|
transform: scale(1.02); |
|
} |
|
|
|
.tagline { |
|
font-family: 'Poppins', sans-serif; |
|
text-align: center; |
|
font-size: 1.5rem; |
|
color: var(--light-cream); |
|
font-weight: 300; |
|
margin-bottom: 2.5rem; |
|
opacity: 0.9; |
|
letter-spacing: 0.5px; |
|
} |
|
|
|
/* Card styling */ |
|
.card { |
|
background: rgba(63, 79, 68, 0.25); |
|
backdrop-filter: blur(8px); |
|
-webkit-backdrop-filter: blur(8px); |
|
border-radius: 16px; |
|
border: 1px solid rgba(162, 123, 92, 0.2); |
|
padding: 28px; |
|
margin-bottom: 24px; |
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); |
|
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); |
|
} |
|
|
|
.card:hover { |
|
transform: translateY(-5px); |
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); |
|
border: 1px solid rgba(162, 123, 92, 0.4); |
|
} |
|
|
|
|
|
|
|
/* Input field styling */ |
|
.stTextInput > div > div > input, .stTextArea > div > div > textarea { |
|
background-color: rgba(44, 57, 48, 0.8) !important; /* Increased opacity */ |
|
border: 1px solid rgba(162, 123, 92, 0.3) !important; |
|
color: var(--light-cream) !important; |
|
border-radius: 8px !important; |
|
padding: 12px 16px !important; |
|
font-size: 16px !important; |
|
transition: all 0.3s ease !important; |
|
} |
|
|
|
.stTextInput > div > div > input:hover, .stTextArea > div > div > textarea:hover { |
|
background-color: rgba(44, 57, 48, 0.9) !important; /* Even higher opacity on hover */ |
|
border: 1px solid rgba(162, 123, 92, 0.5) !important; /* More visible border on hover */ |
|
} |
|
|
|
.stTextInput > div > div > input:focus, .stTextArea > div > div > textarea:focus { |
|
background-color: rgba(44, 57, 48, 1) !important; /* Full opacity on focus */ |
|
border: 1px solid var(--accent-brown) !important; |
|
box-shadow: 0 0 0 2px rgba(162, 123, 92, 0.2) !important; |
|
} |
|
|
|
/* Select box styling */ |
|
.stSelectbox > div > div { |
|
background-color: rgba(44, 57, 48, 0.6) !important; |
|
border: 1px solid rgba(162, 123, 92, 0.3) !important; |
|
border-radius: 8px !important; |
|
color: var(--light-cream) !important; |
|
} |
|
|
|
.stSelectbox > div > div > div { |
|
color: var(--light-cream) !important; |
|
font-size: 16px !important; |
|
} |
|
|
|
.stSelectbox > div > div:hover { |
|
border: 1px solid var(--accent-brown) !important; |
|
} |
|
|
|
/* Button styling */ |
|
.stButton > button { |
|
background: linear-gradient(135deg, var(--accent-brown) 0%, #8a6a4d 100%) !important; |
|
color: var(--light-cream) !important; |
|
font-weight: 500 !important; |
|
border: none !important; |
|
border-radius: 8px !important; |
|
padding: 0.6rem 1.2rem !important; |
|
font-size: 1rem !important; |
|
transition: all 0.3s ease !important; |
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important; |
|
text-transform: uppercase !important; |
|
letter-spacing: 0.5px !important; |
|
} |
|
|
|
.stButton > button:hover { |
|
transform: translateY(-2px) !important; |
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15) !important; |
|
background: linear-gradient(135deg, #b38b68 0%, var(--accent-brown) 100%) !important; |
|
} |
|
|
|
.stButton > button:active { |
|
transform: translateY(1px) !important; |
|
} |
|
|
|
/* Generate button styling */ |
|
div[data-testid="element-container"]:has(button#generate_button) button { |
|
background: linear-gradient(135deg, var(--medium-green) 0%, var(--dark-green) 100%) !important; |
|
border: 1px solid var(--accent-brown) !important; |
|
color: var(--light-cream) !important; |
|
font-weight: 600 !important; |
|
font-size: 1.2rem !important; |
|
padding: 0.8rem 1.6rem !important; |
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15) !important; |
|
position: relative; |
|
overflow: hidden; |
|
} |
|
|
|
div[data-testid="element-container"]:has(button#generate_button) button:before { |
|
content: ''; |
|
position: absolute; |
|
top: 0; |
|
left: -100%; |
|
width: 100%; |
|
height: 100%; |
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); |
|
transition: 0.5s; |
|
} |
|
|
|
div[data-testid="element-container"]:has(button#generate_button) button:hover:before { |
|
left: 100%; |
|
} |
|
|
|
/* Label styling */ |
|
.input-label { |
|
color: var(--accent-brown); |
|
font-size: 1rem; |
|
font-weight: 500; |
|
margin-bottom: 0.5rem; |
|
display: block; |
|
} |
|
|
|
/* Subheader styling */ |
|
.stSubheader, .css-10trblm { |
|
color: var(--accent-brown) !important; |
|
font-size: 1.5rem !important; |
|
font-weight: 600 !important; |
|
margin: 1rem 0 !important; |
|
letter-spacing: 0.5px !important; |
|
} |
|
|
|
/* Success/Error message styling */ |
|
.success { |
|
background-color: rgba(46, 125, 50, 0.1); |
|
color: #81c784; |
|
font-size: 1rem; |
|
font-weight: 500; |
|
padding: 1rem; |
|
border-radius: 8px; |
|
border-left: 4px solid #4caf50; |
|
margin-top: 1rem; |
|
} |
|
|
|
.error { |
|
background-color: rgba(211, 47, 47, 0.1); |
|
color: #e57373; |
|
font-size: 1rem; |
|
font-weight: 500; |
|
padding: 1rem; |
|
border-radius: 8px; |
|
border-left: 4px solid #f44336; |
|
margin-top: 1rem; |
|
} |
|
|
|
/* Audio recording indication */ |
|
.stMarkdown p { |
|
font-size: 1rem; |
|
color: var(--light-cream); |
|
} |
|
|
|
/* JSON display styling */ |
|
.element-container .stJson { |
|
background-color: rgba(44, 57, 48, 0.7) !important; |
|
border-radius: 8px !important; |
|
border: 1px solid rgba(162, 123, 92, 0.3) !important; |
|
} |
|
|
|
/* Divider styling */ |
|
hr { |
|
border: 0; |
|
height: 1px; |
|
background: linear-gradient(to right, transparent, var(--accent-brown), transparent); |
|
margin: 2rem 0; |
|
} |
|
|
|
/* Animation for the listening text */ |
|
@keyframes pulse { |
|
0% { opacity: 0.6; } |
|
50% { opacity: 1; } |
|
100% { opacity: 0.6; } |
|
} |
|
|
|
.listening { |
|
animation: pulse 1.5s infinite; |
|
color: var(--accent-brown); |
|
font-weight: 500; |
|
} |
|
|
|
/* Scrollbar styling */ |
|
::-webkit-scrollbar { |
|
width: 8px; |
|
height: 8px; |
|
} |
|
|
|
::-webkit-scrollbar-track { |
|
background: var(--dark-green); |
|
} |
|
|
|
::-webkit-scrollbar-thumb { |
|
background: var(--accent-brown); |
|
border-radius: 4px; |
|
} |
|
|
|
::-webkit-scrollbar-thumb:hover { |
|
background: #8a6a4d; |
|
} |
|
</style> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
st.markdown('<div class="title">BillBot</div>', unsafe_allow_html=True) |
|
st.markdown('<div class="tagline">Speak, and Let AI Create Your Bill</div>', unsafe_allow_html=True) |
|
|
|
|
|
col1, col2 = st.columns([1, 3]) |
|
|
|
|
|
with col1: |
|
|
|
|
|
st.markdown("<p class='input-label'>Speech Recognition Language</p>", unsafe_allow_html=True) |
|
language_options = ['English', 'Urdu'] |
|
selected_language = st.selectbox(" ", language_options, label_visibility="collapsed") |
|
|
|
st.markdown("<div style='height:24px;'></div>", unsafe_allow_html=True) |
|
|
|
st.markdown("<p class='input-label'>Select Currency</p>", unsafe_allow_html=True) |
|
currency_options = ['USD', 'PKR'] |
|
selected_currency = st.selectbox(" ", currency_options, label_visibility="collapsed") |
|
st.session_state.currency = selected_currency |
|
|
|
st.markdown('</div>', unsafe_allow_html=True) |
|
|
|
|
|
with col2: |
|
|
|
|
|
st.markdown("<p class='stSubheader'>Customer Name</p>", unsafe_allow_html=True) |
|
customer_name = st.text_area("Enter Customer Name", st.session_state.customer_name, height=70, key="name_input", label_visibility="collapsed") |
|
st.session_state.customer_name = customer_name |
|
|
|
col_record, col_space = st.columns([1, 1]) |
|
with col_record: |
|
record_name_btn = st.button("Record Name", key="name_button") |
|
|
|
if record_name_btn: |
|
toggle_listen("name_button") |
|
if st.session_state.is_listening: |
|
recognized_name = recognize_speech(language_map[selected_language]) |
|
if recognized_name: |
|
st.session_state.customer_name = recognized_name |
|
st.markdown('</div>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
st.markdown("<p class='stSubheader'>Customer Number</p>", unsafe_allow_html=True) |
|
customer_number = st.text_area("Enter Customer Number", st.session_state.customer_number, height=70, key="number_input", label_visibility="collapsed") |
|
st.session_state.customer_number = customer_number |
|
|
|
col_record, col_space = st.columns([1, 1]) |
|
with col_record: |
|
record_number_btn = st.button("Record Number", key="number_button") |
|
|
|
if record_number_btn: |
|
toggle_listen("number_button") |
|
if st.session_state.is_listening: |
|
recognized_number = recognize_speech(language_map[selected_language]) |
|
if recognized_number: |
|
recognized_number = convert_number_words_to_digits(recognized_number, selected_language) |
|
st.session_state.customer_number = recognized_number |
|
st.markdown('</div>', unsafe_allow_html=True) |
|
|
|
|
|
|
|
st.markdown("<p class='stSubheader'>Bill Content</p>", unsafe_allow_html=True) |
|
bill_content = st.text_area("Enter Bill Content", st.session_state.bill_content, height=140, key="bill_input", label_visibility="collapsed") |
|
st.session_state.bill_content = bill_content |
|
|
|
col_record, col_space = st.columns([1, 1]) |
|
with col_record: |
|
record_content_btn = st.button("Record Bill Content", key="content_button") |
|
|
|
if record_content_btn: |
|
toggle_listen("content_button") |
|
if st.session_state.is_listening: |
|
recognized_bill_content = recognize_speech(language_map[selected_language]) |
|
if recognized_bill_content: |
|
recognized_bill_content = convert_number_words_to_digits(recognized_bill_content, selected_language) |
|
st.session_state.bill_content = recognized_bill_content |
|
st.markdown('</div>', unsafe_allow_html=True) |
|
|
|
|
|
st.markdown('<div style="margin-top:2rem;"></div>', unsafe_allow_html=True) |
|
_, center_col, _ = st.columns([1, 2, 1]) |
|
with center_col: |
|
generate_btn = st.button("Generate and Send Bill", key="generate_button", use_container_width=True) |
|
|
|
|
|
if generate_btn: |
|
if st.session_state.customer_name and st.session_state.customer_number and st.session_state.bill_content: |
|
|
|
structured_bill_content = extract_item_details_from_gemini(st.session_state.bill_content) |
|
|
|
if structured_bill_content: |
|
|
|
pdf_file = generate_invoice_pdf( |
|
st.session_state.customer_name, |
|
st.session_state.customer_number, |
|
structured_bill_content, |
|
st.session_state.currency |
|
) |
|
|
|
if pdf_file: |
|
result = send_pdf_via_whatsapp(pdf_file, st.session_state.customer_number) |
|
st.markdown(f"<div class='success'>{result}</div>", unsafe_allow_html=True) |
|
else: |
|
st.markdown("<div class='error'>Error: Could not generate invoice PDF.</div>", unsafe_allow_html=True) |
|
else: |
|
st.markdown("<div class='error'>Error: Could not extract structured content from bill text.</div>", unsafe_allow_html=True) |
|
else: |
|
st.markdown("<div class='error'>Please fill in all required fields: Customer Name, Customer Number, and Bill Content.</div>", unsafe_allow_html=True) |
|
|
|
|