AI_Calling / app.py
theakash015's picture
fix rerun
6864b9f verified
import os
import time
import requests
import streamlit as st
import json
# ============ CONFIGURATION FOR HUGGING FACE ============
def get_env(key, default=""):
"""Get environment variable from either Hugging Face secrets or local .env"""
value = os.environ.get(key)
if value:
return value
try:
import dotenv
dotenv.load_dotenv(verbose=True)
return os.getenv(key, default)
except:
return default
# Backend server configuration
SERVER = get_env("SERVER", "ai-calling-gyhz.onrender.com")
DEFAULT_PHONE = get_env("YOUR_NUMBER", "")
DEFAULT_SYSTEM_MESSAGE = get_env("SYSTEM_MESSAGE", "You are a helpful AI assistant making a phone call. Be respectful, concise, and helpful.")
DEFAULT_INITIAL_MESSAGE = get_env("INITIAL_MESSAGE", "Hello, this is an AI assistant calling. How can I help you today?")
# ========================================================
# Page configuration with custom theme
st.set_page_config(
page_title="VoiceGenius AI Calling",
page_icon="๐Ÿ“ž",
layout="wide",
initial_sidebar_state="expanded"
)
# Apply custom CSS
st.markdown("""
<style>
.main {
background-color: #f5f7fa;
}
.block-container {
padding-top: 2rem;
padding-bottom: 2rem;
}
.stButton button {
border-radius: 20px;
font-weight: 500;
padding: 0.5rem 1.5rem;
transition: all 0.3s ease;
}
.stButton button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.stTextInput input, .stTextArea textarea {
border-radius: 10px;
border: 1px solid #e0e0e0;
}
h1, h2, h3 {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.call-card {
background-color: white;
border-radius: 15px;
padding: 1.5rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
margin-bottom: 1rem;
}
.template-card {
cursor: pointer;
transition: all 0.2s ease;
}
.template-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 15px rgba(0,0,0,0.1);
}
.transcript-message {
padding: 10px 15px;
border-radius: 18px;
margin-bottom: 10px;
max-width: 80%;
line-height: 1.4;
}
.user-message {
background-color: #ed9121;
margin-left: auto;
border-bottom-right-radius: 5px;
}
.assistant-message {
background-color: #2e7ef7;
color: white;
margin-right: auto;
border-bottom-left-radius: 5px;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
.status-active {
background-color: #4CAF50;
animation: pulse 1.5s infinite;
}
.status-inactive {
background-color: #9e9e9e;
}
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
.logo-text {
font-weight: 700;
background: linear-gradient(90deg, #2e7ef7, #1a56c5);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.stat-card {
text-align: center;
padding: 1rem;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
.prompt-card {
padding: 15px;
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
height: 100%;
}
.prompt-card:hover {
transform: translateY(-3px);
}
.prompt-medical {
background: linear-gradient(135deg, #ff9a9e, #fad0c4);
}
.prompt-finance {
background: linear-gradient(135deg, #84fab0, #8fd3f4);
}
.prompt-sports {
background: linear-gradient(135deg, #a1c4fd, #c2e9fb);
}
.prompt-custom {
background: linear-gradient(135deg, #e0c3fc, #8ec5fc);
}
</style>
""", unsafe_allow_html=True)
# Initialize session state variables
if 'call_active' not in st.session_state:
st.session_state.call_active = False
st.session_state.call_sid = None
st.session_state.transcript = []
st.session_state.system_message = DEFAULT_SYSTEM_MESSAGE
st.session_state.initial_message = DEFAULT_INITIAL_MESSAGE
st.session_state.all_transcripts = []
st.session_state.recording_info = None
st.session_state.call_selector = "Current Call"
st.session_state.total_calls = 0
st.session_state.successful_calls = 0
st.session_state.call_duration = 0
st.session_state.selected_template = None
st.session_state.should_reset_selector = False
# Predefined prompt templates
prompt_templates = {
"medical": {
"system": {
"en": "You are a medical appointment scheduling assistant for Dr. Smith's office. You should collect patient information, reason for appointment, and preferred times. You cannot give medical advice.",
"es": "Eres un asistente de programaciรณn de citas mรฉdicas para la oficina del Dr. Smith. Debes recopilar informaciรณn del paciente, motivo de la cita y horarios preferidos. No puedes dar consejos mรฉdicos."
},
"initial": {
"en": "Hello, this is the virtual assistant from Dr. Smith's office.",
"es": "Hola, soy el asistente virtual de la oficina del Dr. Smith"
}
},
"finance": {
"system": {
"en": "You are a financial services assistant for GrowWealth Advisors. You can discuss appointment scheduling and general services offered, but cannot give specific investment advice during this call.",
"es": "Eres un asistente de servicios financieros para GrowWealth Advisors. Puedes discutir la programaciรณn de citas y los servicios generales ofrecidos, pero no puedes dar consejos especรญficos de inversiรณn durante esta llamada."
},
"initial": {
"en": "Hello, this is the virtual assistant from GrowWealth Financial Advisors.",
"es": "Hola, soy el asistente virtual de GrowWealth Financial Advisors."
}
},
"sports": {
"system": {
"en": "You are a membership coordinator for SportsFit Gym. You should provide information about membership options, facility hours, and classes offered. Be enthusiastic and encouraging.",
"es": "Eres un coordinador de membresรญas para el gimnasio SportsFit. Debes proporcionar informaciรณn sobre las opciones de membresรญa, horarios de las instalaciones y clases ofrecidas. Sรฉ entusiasta y alentador."
},
"initial": {
"en": "Hi there! This is the virtual assistant from SportsFit Gym.",
"es": "ยกHola! Soy el asistente virtual del gimnasio SportsFit."
}
},
"customer_service": {
"system": {
"en": "You are a customer service representative following up on a recent purchase. You should check satisfaction levels, address any concerns, and offer assistance if needed.",
"es": "Eres un representante de servicio al cliente dando seguimiento a una compra reciente. Debes verificar los niveles de satisfacciรณn, atender cualquier inquietud y ofrecer asistencia si es necesario."
},
"initial": {
"en": "Hello, this is the customer service team from Acme Products.",
"es": "Hola, soy del equipo de servicio al cliente de Acme Products."
}
}
}
voice_options = {
"en": { # English voices
"Emma (Female)": "11labs_emma",
"Daniel (Male)": "11labs_daniel",
"Rachel (Female)": "11labs_rachel",
"John (Male)": "11labs_john"
},
"es": { # Spanish voices
"Sofia (Female)": "11labs_sofia",
"Miguel (Male)": "11labs_miguel",
"Isabella (Female)": "11labs_isabella",
"Carlos (Male)": "11labs_carlos",
"JuanRestrepoPro (Male)": "11labs_JuanRestrepoPro"
}
}
# Helper functions
def fetch_all_transcripts():
try:
response = requests.get(f"https://{SERVER}/all_transcripts")
transcripts = response.json().get('transcripts', [])
st.session_state.total_calls = len(transcripts)
st.session_state.successful_calls = sum(1 for t in transcripts if any(m['role'] == 'user' for m in t.get('transcript', [])))
return transcripts
except requests.RequestException as e:
st.error(f"Error fetching call list: {str(e)}")
return []
def format_duration(seconds):
if seconds < 60:
return f"{seconds}s"
minutes = seconds // 60
remaining_seconds = seconds % 60
return f"{minutes}m {remaining_seconds}s"
def apply_template(template_name):
if template_name in prompt_templates:
lang_code = language_options[selected_language]
st.session_state.system_message = prompt_templates[template_name]["system"].get(lang_code, prompt_templates[template_name]["system"]["en"])
st.session_state.initial_message = prompt_templates[template_name]["initial"].get(lang_code, prompt_templates[template_name]["initial"]["en"])
st.session_state.selected_template = template_name
return True
return False
def fetch_recording_info(call_sid):
try:
response = requests.get(f"https://{SERVER}/call_recording/{call_sid}")
if media_url := response.json().get('recording_url'):
media_response = requests.get(media_url)
if media_response.status_code == 200:
media_data = media_response.json()
return {
'url': f"{media_data.get('media_url')}.mp3",
'duration': media_data.get('duration', 0)
}
except requests.RequestException as e:
st.error(f"Error fetching recording info: {str(e)}")
return None
def on_call_selector_change():
if st.session_state.call_selector != "Current Call":
selected_transcript = next((t for t in st.session_state.all_transcripts if f"Call {t['call_sid']}" == st.session_state.call_selector), None)
if selected_transcript:
st.session_state.recording_info = fetch_recording_info(selected_transcript['call_sid'])
else:
st.warning("No transcript found for the selected call.")
else:
st.session_state.recording_info = None
# Check if we need to reset the selector
if st.session_state.should_reset_selector:
st.session_state.call_selector = "Current Call"
st.session_state.should_reset_selector = False
# Sidebar content
with st.sidebar:
st.markdown(f"""
<div style="text-align: center; margin-bottom: 20px;">
<h1 class="logo-text" style="font-size: 2.2em;">VoiceGenius</h1>
<p style="opacity: 0.7; margin-top: -10px;">AI-Powered Phone Calls</p>
</div>
""", unsafe_allow_html=True)
st.divider()
# Call stats display
col1, col2 = st.columns(2)
with col1:
st.markdown("""
<div class="stat-card">
<h3 style="margin: 0; font-size: 1.8rem; color: #2e7ef7;">๐Ÿ“Š</h3>
<h3 style="margin: 0;">Total Calls</h3>
<p style="font-size: 1.5rem; font-weight: bold;">{}</p>
</div>
""".format(st.session_state.total_calls), unsafe_allow_html=True)
with col2:
st.markdown("""
<div class="stat-card">
<h3 style="margin: 0; font-size: 1.8rem; color: #4CAF50;">โœ…</h3>
<h3 style="margin: 0;">Completed</h3>
<p style="font-size: 1.5rem; font-weight: bold;">{}</p>
</div>
""".format(st.session_state.successful_calls), unsafe_allow_html=True)
st.divider()
# Phone number input
phone_number = st.text_input(
"Phone Number",
placeholder="+1XXXXXXXXXX",
value=DEFAULT_PHONE,
help="Enter the phone number to call in international format"
)
# Language selection
language_options = {
"English": "en",
"Spanish": "es"
}
selected_language = st.selectbox(
"Call Language",
options=list(language_options.keys()),
index=0,
help="Select the language for the conversation"
)
language_code = language_options[selected_language]
selected_lang_code = language_options[selected_language]
# AI Model selection
model_options = {
"OpenAI GPT-4o": "openai",
"Anthropic Claude": "anthropic"
}
selected_model = st.selectbox(
"AI Model",
options=list(model_options.keys()),
index=0,
help="Select the AI language model to use"
)
model_code = model_options[selected_model]
# Store the model selection in session state
if 'model_selection' not in st.session_state:
st.session_state.model_selection = model_code
else:
st.session_state.model_selection = model_code
# Voice selection follows below
available_voices = voice_options.get(selected_lang_code, voice_options["en"])
# Create the voice selection dropdown
selected_voice_name = st.selectbox(
"Voice",
options=list(available_voices.keys()),
index=0,
help="Select the voice for the AI assistant"
)
selected_voice_id = available_voices[selected_voice_name]
# Store the voice_id in session state for use in calls
if 'voice_id' not in st.session_state:
st.session_state.voice_id = selected_voice_id
else:
st.session_state.voice_id = selected_voice_id
st.divider()
# Voice Settings - Add an expander for voice customization
with st.sidebar.expander("Voice Settings ", expanded=False):
st.markdown("### Customize Voice Parameters")
# Initialize voice settings in session state if not present
if 'voice_settings' not in st.session_state:
st.session_state.voice_settings = {
"stability": 0.5,
"similarity_boost": 0.75,
"style": 0.0,
"use_speaker_boost": True,
"speed": 1.0
}
# Add sliders for each voice parameter
st.session_state.voice_settings["stability"] = st.slider(
"Stability",
min_value=0.0,
max_value=1.0,
value=st.session_state.voice_settings.get("stability", 0.5),
step=0.05,
help="Higher values make the voice more consistent between re-generations but can reduce expressiveness."
)
st.session_state.voice_settings["similarity_boost"] = st.slider(
"Similarity Boost",
min_value=0.0,
max_value=1.0,
value=st.session_state.voice_settings.get("similarity_boost", 0.75),
step=0.05,
help="Higher values make the voice more similar to the original voice but can reduce quality."
)
st.session_state.voice_settings["style"] = st.slider(
"Style",
min_value=0.0,
max_value=1.0,
value=st.session_state.voice_settings.get("style", 0.0),
step=0.05,
help="Higher values amplify unique speaking style of the cloned voice."
)
st.session_state.voice_settings["speed"] = st.slider(
"Speed",
min_value=0.7,
max_value=1.2,
value=st.session_state.voice_settings.get("speed", 1.0),
step=0.01,
help="Adjust the speaking speed of the voice."
)
st.session_state.voice_settings["use_speaker_boost"] = st.checkbox(
"Speaker Boost",
value=st.session_state.voice_settings.get("use_speaker_boost", True),
help="Improves voice clarity and target speaker similarity."
)
if st.button("Reset to Defaults"):
st.session_state.voice_settings = {
"stability": 0.5,
"similarity_boost": 0.75,
"style": 0.0,
"use_speaker_boost": True,
"speed": 1.0
}
st.rerun()
st.divider()
# Status indicator
status_class = "status-active" if st.session_state.call_active else "status-inactive"
status_text = "Call in progress" if st.session_state.call_active else "Ready to call"
st.markdown(f"""
<div style="display: flex; align-items: center; margin-bottom: 15px;">
<span class="status-indicator {status_class}"></span>
<span>{status_text}</span>
</div>
""", unsafe_allow_html=True)
# Call controls
call_col1, call_col2 = st.columns(2)
with call_col1:
start_call = st.button("๐Ÿ“ž Start Call", disabled=st.session_state.call_active, use_container_width=True)
with call_col2:
end_call = st.button("๐Ÿ”ด End Call", disabled=not st.session_state.call_active, use_container_width=True)
st.divider()
# Call history
st.subheader("Call History")
st.session_state.all_transcripts = fetch_all_transcripts()
st.selectbox(
"Select a call",
options=["Current Call"] + [f"Call {t['call_sid']}" for t in st.session_state.all_transcripts],
key="call_selector",
index=0,
disabled=st.session_state.call_active,
on_change=on_call_selector_change
)
if st.button("๐Ÿ”„ Refresh Calls", use_container_width=True):
try:
st.session_state.all_transcripts = fetch_all_transcripts()
on_call_selector_change()
except requests.RequestException as e:
st.error(f"Error fetching call list: {str(e)}")
# Main content area
st.markdown("<h1 style='text-align: center;'>AI Voice Calling Assistant</h1>", unsafe_allow_html=True)
# Prompt template selection
st.subheader("Select Prompt Template")
prompt_cols = st.columns(4)
with prompt_cols[0]:
medical_card = st.markdown("""
<div class="prompt-card prompt-medical">
<h3>๐Ÿฉบ Medical Office</h3>
<p>Schedule appointments and collect patient information</p>
</div>
""", unsafe_allow_html=True)
if medical_card:
if st.button("Select Medical", key="medical_btn", use_container_width=True):
apply_template("medical")
with prompt_cols[1]:
finance_card = st.markdown("""
<div class="prompt-card prompt-finance">
<h3>๐Ÿ’น Financial Services</h3>
<p>Schedule consultations and discuss available services</p>
</div>
""", unsafe_allow_html=True)
if finance_card:
if st.button("Select Finance", key="finance_btn", use_container_width=True):
apply_template("finance")
with prompt_cols[2]:
sports_card = st.markdown("""
<div class="prompt-card prompt-sports">
<h3>๐Ÿ‹๏ธ Sports & Fitness</h3>
<p>Discuss gym memberships and class schedules</p>
</div>
""", unsafe_allow_html=True)
if sports_card:
if st.button("Select Sports", key="sports_btn", use_container_width=True):
apply_template("sports")
with prompt_cols[3]:
custom_card = st.markdown("""
<div class="prompt-card prompt-custom">
<h3>โœจ Customer Service</h3>
<p>Follow up on purchases and customer satisfaction</p>
</div>
""", unsafe_allow_html=True)
if custom_card:
if st.button("Select Customer Service", key="custom_btn", use_container_width=True):
apply_template("customer_service")
st.divider()
# Custom prompt inputs in an expandable section
with st.expander("Customize AI Instructions", expanded=st.session_state.selected_template is None):
st.session_state.system_message = st.text_area(
"System Instructions (AI's role and guidelines)",
value=st.session_state.system_message,
disabled=st.session_state.call_active,
height=100
)
st.session_state.initial_message = st.text_area(
"Initial Message (First thing the AI will say)",
value=st.session_state.initial_message,
disabled=st.session_state.call_active,
height=100
)
st.divider()
# Handle call actions
if start_call and phone_number and not st.session_state.call_active:
with st.spinner(f"๐Ÿ“ž Calling {phone_number}..."):
try:
response = requests.post(f"https://{SERVER}/start_call", json={
"to_number": phone_number,
"system_message": st.session_state.system_message,
"initial_message": st.session_state.initial_message,
"language": language_code,
"voice_id": st.session_state.voice_id,
"model": st.session_state.model_selection,
"voice_settings": json.dumps(st.session_state.voice_settings)
}, timeout=10)
call_data = response.json()
if call_sid := call_data.get('call_sid'):
st.session_state.call_sid = call_sid
st.session_state.transcript = []
# Create progress bar for connecting
progress_bar = st.progress(0)
connection_status = st.empty()
for i in range(60):
progress_value = min(i / 30, 1.0) # Max at 30 seconds
progress_bar.progress(progress_value)
connection_status.info(f"Establishing connection... ({i+1}s)")
time.sleep(1)
status = requests.get(f"https://{SERVER}/call_status/{call_sid}").json().get('status')
if status == 'in-progress':
progress_bar.progress(1.0)
connection_status.success("Call connected!")
st.session_state.call_active = True
# Set flag to reset selector on next rerun instead of modifying directly
st.session_state.should_reset_selector = True
time.sleep(1)
st.rerun()
break
if status in ['completed', 'failed', 'busy', 'no-answer']:
progress_bar.empty()
connection_status.error(f"Call ended: {status}")
break
else:
progress_bar.empty()
connection_status.error("Timeout waiting for call to connect.")
else:
st.error(f"Failed to initiate call: {call_data}")
except requests.RequestException as e:
st.error(f"Error: {str(e)}")
elif start_call and not phone_number:
st.warning("Please enter a valid phone number.")
if end_call:
try:
with st.spinner("Ending call..."):
response = requests.post(f"https://{SERVER}/end_call", json={"call_sid": st.session_state.call_sid})
if response.status_code == 200:
st.success("Call ended successfully.")
st.session_state.call_active = False
st.session_state.call_sid = None
time.sleep(1)
st.rerun()
else:
st.error(f"Failed to end call: {response.text}")
except requests.RequestException as e:
st.error(f"Error ending call: {str(e)}")
# Display call transcript or recording
st.subheader("Call Transcript")
# Call Recording display
if st.session_state.call_selector != "Current Call" and st.session_state.recording_info:
st.markdown(f"""
<div class="call-card">
<h3>๐Ÿ“ž Call Recording</h3>
<p>Duration: {format_duration(st.session_state.recording_info['duration'])}</p>
</div>
""", unsafe_allow_html=True)
st.audio(st.session_state.recording_info['url'], format="audio/mp3", start_time=0)
# Display transcript in chat-like format
transcript_container = st.container()
with transcript_container:
if st.session_state.call_active and st.session_state.call_sid:
for entry in st.session_state.transcript:
if entry['role'] == 'user':
st.markdown(f"""
<div class="transcript-message user-message">
<strong>Caller:</strong> {entry['content']}
</div>
""", unsafe_allow_html=True)
elif entry['role'] == 'assistant':
st.markdown(f"""
<div class="transcript-message assistant-message">
<strong>AI:</strong> {entry['content']}
</div>
""", unsafe_allow_html=True)
elif st.session_state.call_selector != "Current Call":
if transcript := next((t for t in st.session_state.all_transcripts if f"Call {t['call_sid']}" == st.session_state.call_selector), None):
for entry in transcript['transcript']:
if entry['role'] == 'user':
st.markdown(f"""
<div class="transcript-message user-message">
<strong>Caller:</strong> {entry['content']}
</div>
""", unsafe_allow_html=True)
elif entry['role'] == 'assistant':
st.markdown(f"""
<div class="transcript-message assistant-message">
<strong>AI:</strong> {entry['content']}
</div>
""", unsafe_allow_html=True)
else:
st.info("No call transcript available. Start a call or select a previous call from the sidebar.")
# Live call updates
if st.session_state.call_active and st.session_state.call_sid:
def update_call_info():
try:
status = requests.get(f"https://{SERVER}/call_status/{st.session_state.call_sid}", timeout=5).json().get('status')
if status not in ['in-progress', 'ringing']:
st.session_state.call_active = False
st.warning(f"Call ended: {status}")
return False
transcript_data = requests.get(f"https://{SERVER}/transcript/{st.session_state.call_sid}", timeout=5).json()
if transcript_data.get('call_ended', False):
st.session_state.call_active = False
st.info(f"Call ended. Status: {transcript_data.get('final_status', 'Unknown')}")
return False
st.session_state.transcript = transcript_data.get('transcript', [])
return True
except requests.RequestException as e:
st.error(f"Error updating call info: {str(e)}")
return False
# Only update if call is truly active
if update_call_info():
time.sleep(2) # Increased delay to reduce API calls
st.rerun()
else:
# Call ended - clean up
st.session_state.call_active = False
st.session_state.call_sid = None
st.info("Call has ended. You can start a new call if needed.")
# Footer
st.markdown("""
<div style="text-align: center; margin-top: 50px; opacity: 0.7;">
<p>VoiceGenius AI Calling Platform โ€ข v2.0.3</p>
</div>
""", unsafe_allow_html=True)