|
from streamlit_webrtc import webrtc_streamer, WebRtcMode, AudioProcessorBase |
|
from sentiment_analysis import analyze_sentiment, transcribe_with_chunks |
|
from product_recommender import ProductRecommender |
|
from objection_handler import ObjectionHandler |
|
from google_sheets import fetch_call_data, store_data_in_sheet |
|
from sentence_transformers import SentenceTransformer |
|
from env_setup import config |
|
import re |
|
import uuid |
|
import pandas as pd |
|
import plotly.express as px |
|
import streamlit as st |
|
import numpy as np |
|
import queue |
|
import threading |
|
|
|
|
|
objection_handler = ObjectionHandler("objections.csv") |
|
product_recommender = ProductRecommender("recommendations.csv") |
|
model = SentenceTransformer('all-MiniLM-L6-v2') |
|
|
|
|
|
transcription_queue = queue.Queue() |
|
|
|
def generate_comprehensive_summary(chunks): |
|
|
|
pass |
|
|
|
def is_valid_input(text): |
|
|
|
pass |
|
|
|
def is_relevant_sentiment(sentiment_score): |
|
|
|
pass |
|
|
|
def calculate_overall_sentiment(sentiment_scores): |
|
|
|
pass |
|
|
|
def handle_objection(text): |
|
query_embedding = model.encode([text]) |
|
distances, indices = objection_handler.index.search(query_embedding, 1) |
|
if distances[0][0] < 1.5: |
|
responses = objection_handler.handle_objection(text) |
|
return "\n".join(responses) if responses else "No objection response found." |
|
return "No objection response found." |
|
|
|
class AudioProcessor(AudioProcessorBase): |
|
def __init__(self): |
|
self.sr = 16000 |
|
self.q = transcription_queue |
|
|
|
def recv(self, frame): |
|
audio_data = frame.to_ndarray() |
|
audio_bytes = (audio_data * 32767).astype(np.int16).tobytes() |
|
|
|
print(f"Audio data shape: {audio_data.shape}") |
|
print(f"Audio data sample: {audio_data[:10]}") |
|
|
|
text = self.transcribe_audio(audio_bytes) |
|
if text: |
|
self.q.put(text) |
|
|
|
return frame |
|
|
|
def transcribe_audio(self, audio_bytes): |
|
try: |
|
chunks = transcribe_with_chunks({}) |
|
if chunks: |
|
return chunks[-1][0] |
|
except Exception as e: |
|
print(f"Error transcribing audio: {e}") |
|
return None |
|
|
|
def real_time_analysis(): |
|
st.info("Listening... Say 'stop' to end the process.") |
|
|
|
webrtc_ctx = webrtc_streamer( |
|
key="real-time-audio", |
|
mode=WebRtcMode.SENDONLY, |
|
audio_processor_factory=AudioProcessor, |
|
media_stream_constraints={"audio": True, "video": False}, |
|
) |
|
|
|
if webrtc_ctx.state.playing: |
|
while not transcription_queue.empty(): |
|
text = transcription_queue.get() |
|
st.write(f"*Recognized Text:* {text}") |
|
|
|
sentiment, score = analyze_sentiment(text) |
|
st.write(f"*Sentiment:* {sentiment} (Score: {score})") |
|
|
|
objection_response = handle_objection(text) |
|
st.write(f"*Objection Response:* {objection_response}") |
|
|
|
recommendations = [] |
|
if is_valid_input(text) and is_relevant_sentiment(score): |
|
query_embedding = model.encode([text]) |
|
distances, indices = product_recommender.index.search(query_embedding, 1) |
|
|
|
if distances[0][0] < 1.5: |
|
recommendations = product_recommender.get_recommendations(text) |
|
|
|
if recommendations: |
|
st.write("*Product Recommendations:*") |
|
for rec in recommendations: |
|
st.write(rec) |
|
|
|
def fetch_data_and_display(): |
|
try: |
|
st.header("Call Summaries and Sentiment Analysis") |
|
data = fetch_call_data(config["google_sheet_id"]) |
|
|
|
print(f"Fetched data: {data}") |
|
|
|
if data.empty: |
|
st.warning("No data available in the Google Sheet.") |
|
else: |
|
sentiment_counts = data['Sentiment'].value_counts() |
|
|
|
col1, col2 = st.columns(2) |
|
with col1: |
|
st.subheader("Sentiment Distribution") |
|
fig_pie = px.pie( |
|
values=sentiment_counts.values, |
|
names=sentiment_counts.index, |
|
title='Call Sentiment Breakdown', |
|
color_discrete_map={ |
|
'POSITIVE': 'green', |
|
'NEGATIVE': 'red', |
|
'NEUTRAL': 'blue' |
|
} |
|
) |
|
st.plotly_chart(fig_pie) |
|
|
|
with col2: |
|
st.subheader("Sentiment Counts") |
|
fig_bar = px.bar( |
|
x=sentiment_counts.index, |
|
y=sentiment_counts.values, |
|
title='Number of Calls by Sentiment', |
|
labels={'x': 'Sentiment', 'y': 'Number of Calls'}, |
|
color=sentiment_counts.index, |
|
color_discrete_map={ |
|
'POSITIVE': 'green', |
|
'NEGATIVE': 'red', |
|
'NEUTRAL': 'blue' |
|
} |
|
) |
|
st.plotly_chart(fig_bar) |
|
|
|
st.subheader("All Calls") |
|
display_data = data.copy() |
|
display_data['Summary Preview'] = display_data['Summary'].str[:100] + '...' |
|
st.dataframe(display_data[['Call ID', 'Chunk', 'Sentiment', 'Summary Preview', 'Overall Sentiment']]) |
|
|
|
unique_call_ids = data[data['Call ID'] != '']['Call ID'].unique() |
|
call_id = st.selectbox("Select a Call ID to view details:", unique_call_ids) |
|
|
|
call_details = data[data['Call ID'] == call_id] |
|
if not call_details.empty: |
|
st.subheader("Detailed Call Information") |
|
st.write(f"**Call ID:** {call_id}") |
|
st.write(f"**Overall Sentiment:** {call_details.iloc[0]['Overall Sentiment']}") |
|
|
|
st.subheader("Full Call Summary") |
|
st.text_area("Summary:", |
|
value=call_details.iloc[0]['Summary'], |
|
height=200, |
|
disabled=True) |
|
|
|
st.subheader("Conversation Chunks") |
|
for _, row in call_details.iterrows(): |
|
if pd.notna(row['Chunk']): |
|
st.write(f"**Chunk:** {row['Chunk']}") |
|
st.write(f"**Sentiment:** {row['Sentiment']}") |
|
st.write("---") |
|
else: |
|
st.error("No details available for the selected Call ID.") |
|
except Exception as e: |
|
st.error(f"Error loading dashboard: {e}") |
|
|
|
def run_app(): |
|
st.set_page_config(page_title="Sales Call Assistant", layout="wide") |
|
st.title("AI Sales Call Assistant") |
|
st.warning("The space is currently not working due to issues with real-time transcription and data fetching from Google Sheets. We are working on resolving these issues.") |
|
|
|
st.markdown(""" |
|
<style> |
|
/* Header Container Styling */ |
|
.header-container { |
|
background: linear-gradient(135deg, #F8F9FA 0%, #E9ECEF 100%); |
|
padding: 20px; |
|
border-radius: 15px; |
|
margin-bottom: 30px; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
/* Section Container Styling */ |
|
.section { |
|
background: linear-gradient(135deg, #FFFFFF 0%, #F8F9FA 100%); |
|
padding: 25px; |
|
border-radius: 15px; |
|
margin-bottom: 30px; |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
/* Header Text Styling */ |
|
.header { |
|
font-size: 2.5em; |
|
font-weight: 800; |
|
text-align: center; |
|
background: linear-gradient(120deg, #0D6EFD 0%, #0B5ED7 100%); |
|
-webkit-background-clip: text; |
|
-webkit-text-fill-color: transparent; |
|
margin: 0; |
|
padding: 10px; |
|
letter-spacing: 1px; |
|
} |
|
|
|
/* Subheader Styling */ |
|
.subheader { |
|
font-size: 1.8em; |
|
font-weight: 600; |
|
background: linear-gradient(120deg, #0D6EFD 0%, #0B5ED7 100%); |
|
-webkit-background-clip: text; |
|
-webkit-text-fill-color: transparent; |
|
margin-top: 20px; |
|
margin-bottom: 10px; |
|
text-align: left; |
|
} |
|
|
|
/* Table Container Styling */ |
|
.table-container { |
|
background: linear-gradient(135deg, #FFFFFF 0%, #F8F9FA 100%); |
|
padding: 20px; |
|
border-radius: 10px; |
|
margin: 20px 0; |
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
/* Dark mode adjustments */ |
|
@media (prefers-color-scheme: dark) { |
|
.header-container { |
|
background: linear-gradient(135deg, #212529 0%, #343A40 100%); |
|
} |
|
|
|
.section { |
|
background: linear-gradient(135deg, #212529 0%, #2B3035 100%); |
|
} |
|
|
|
.table-container { |
|
background: linear-gradient(135deg, #212529 0%, #2B3035 100%); |
|
} |
|
|
|
.header { |
|
background: linear-gradient(120deg, #6EA8FE 0%, #9EC5FE 100%); |
|
-webkit-background-clip: text; |
|
-webkit-text-fill-color: transparent; |
|
} |
|
|
|
.subheader { |
|
background: linear-gradient(120deg, #6EA8FE 0%, #9EC5FE 100%); |
|
-webkit-background-clip: text; |
|
-webkit-text-fill-color: transparent; |
|
} |
|
} |
|
|
|
/* Button Styling */ |
|
.stButton > button { |
|
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%); |
|
color: white; |
|
border: none; |
|
padding: 10px 20px; |
|
border-radius: 5px; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.stButton > button:hover { |
|
background: linear-gradient(135deg, #1976D2 0%, #1565C0 100%); |
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); |
|
} |
|
|
|
/* Tab Styling */ |
|
.stTabs [data-baseweb="tab-list"] { |
|
gap: 24px; |
|
background: linear-gradient(135deg, #F8F9FA 0%, #E9ECEF 100%); |
|
padding: 10px; |
|
border-radius: 10px; |
|
} |
|
|
|
.stTabs [data-baseweb="tab"] { |
|
background-color: transparent; |
|
border-radius: 4px; |
|
color: #1976D2; |
|
font-weight: 600; |
|
padding: 10px 16px; |
|
} |
|
|
|
.stTabs [aria-selected="true"] { |
|
background: linear-gradient(120deg, #2196F3 0%, #1976D2 100%); |
|
color: white; |
|
} |
|
|
|
/* Dark mode tab adjustments */ |
|
@media (prefers-color-scheme: dark) { |
|
.stTabs [data-baseweb="tab-list"] { |
|
background: linear-gradient(135deg, #212529 0%, #343A40 100%); |
|
} |
|
|
|
.stTabs [data-baseweb="tab"] { |
|
color: #82B1FF; |
|
} |
|
|
|
.stTabs [aria-selected="true"] { |
|
background: linear-gradient(120deg, #448AFF 0%, #2979FF 100%); |
|
} |
|
} |
|
|
|
/* Message Styling */ |
|
.success { |
|
background: linear-gradient(135deg, #43A047 0%, #2E7D32 100%); |
|
color: white; |
|
padding: 10px; |
|
border-radius: 5px; |
|
margin: 10px 0; |
|
} |
|
|
|
.error { |
|
background: linear-gradient(135deg, #E53935 0%, #C62828 100%); |
|
color: white; |
|
padding: 10px; |
|
border-radius: 5px; |
|
margin: 10px 0; |
|
} |
|
|
|
.warning { |
|
background: linear-gradient(135deg, #FB8C00 0%, #F57C00 100%); |
|
color: white; |
|
padding: 10px; |
|
border-radius: 5px; |
|
margin: 10px 0; |
|
} |
|
</style> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
|
|
st.sidebar.title("Navigation") |
|
app_mode = st.sidebar.radio("Choose a mode:", ["Real-Time Call Analysis", "Dashboard"]) |
|
|
|
if app_mode == "Real-Time Call Analysis": |
|
st.header("Real-Time Sales Call Analysis") |
|
if st.button("Start Listening"): |
|
real_time_analysis() |
|
|
|
elif app_mode == "Dashboard": |
|
st.header("Call Summaries and Sentiment Analysis") |
|
try: |
|
data = fetch_call_data(config["google_sheet_id"]) |
|
if data.empty: |
|
st.warning("No data available in the Google Sheet.") |
|
else: |
|
sentiment_counts = data['Sentiment'].value_counts() |
|
|
|
product_mentions = filter_product_mentions(data[['Chunk']].values.tolist(), product_titles) |
|
product_mentions_df = pd.DataFrame(list(product_mentions.items()), columns=['Product', 'Count']) |
|
|
|
col1, col2 = st.columns(2) |
|
with col1: |
|
st.subheader("Sentiment Distribution") |
|
fig_bar = px.bar( |
|
x=sentiment_counts.index, |
|
y=sentiment_counts.values, |
|
title='Number of Calls by Sentiment', |
|
labels={'x': 'Sentiment', 'y': 'Number of Calls'}, |
|
color=sentiment_counts.index, |
|
color_discrete_map={ |
|
'POSITIVE': 'green', |
|
'NEGATIVE': 'red', |
|
'NEUTRAL': 'blue' |
|
} |
|
) |
|
st.plotly_chart(fig_bar) |
|
|
|
with col2: |
|
st.subheader("Most Mentioned Products") |
|
fig_products = px.pie( |
|
values=product_mentions_df['Count'], |
|
names=product_mentions_df['Product'], |
|
title='Most Mentioned Products' |
|
) |
|
st.plotly_chart(fig_products) |
|
|
|
st.subheader("All Calls") |
|
display_data = data.copy() |
|
display_data['Summary Preview'] = display_data['Summary'].str[:100] + '...' |
|
st.dataframe(display_data[['Call ID', 'Chunk', 'Sentiment', 'Summary Preview', 'Overall Sentiment']]) |
|
|
|
unique_call_ids = data[data['Call ID'] != '']['Call ID'].unique() |
|
call_id = st.selectbox("Select a Call ID to view details:", unique_call_ids) |
|
|
|
call_details = data[data['Call ID'] == call_id] |
|
if not call_details.empty: |
|
st.subheader("Detailed Call Information") |
|
st.write(f"**Call ID:** {call_id}") |
|
st.write(f"**Overall Sentiment:** {call_details.iloc[0]['Overall Sentiment']}") |
|
|
|
st.subheader("Full Call Summary") |
|
st.text_area("Summary:", |
|
value=call_details.iloc[0]['Summary'], |
|
height=200, |
|
disabled=True) |
|
|
|
else: |
|
st.error("No details available for the selected Call ID.") |
|
except Exception as e: |
|
st.error(f"Error loading dashboard: {e}") |
|
|
|
if __name__ == "__main__": |
|
run_app() |