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(""" """, 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"""

VoiceGenius

AI-Powered Phone Calls

""", unsafe_allow_html=True) st.divider() # Call stats display col1, col2 = st.columns(2) with col1: st.markdown("""

📊

Total Calls

{}

""".format(st.session_state.total_calls), unsafe_allow_html=True) with col2: st.markdown("""

Completed

{}

""".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"""
{status_text}
""", 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("

AI Voice Calling Assistant

", 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("""

🩺 Medical Office

Schedule appointments and collect patient information

""", 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("""

💹 Financial Services

Schedule consultations and discuss available services

""", 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("""

🏋️ Sports & Fitness

Discuss gym memberships and class schedules

""", 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("""

✨ Customer Service

Follow up on purchases and customer satisfaction

""", 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"""

📞 Call Recording

Duration: {format_duration(st.session_state.recording_info['duration'])}

""", 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"""
Caller: {entry['content']}
""", unsafe_allow_html=True) elif entry['role'] == 'assistant': st.markdown(f"""
AI: {entry['content']}
""", 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"""
Caller: {entry['content']}
""", unsafe_allow_html=True) elif entry['role'] == 'assistant': st.markdown(f"""
AI: {entry['content']}
""", 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("""

VoiceGenius AI Calling Platform • v2.0.3

""", unsafe_allow_html=True)