import streamlit as st import os import base64 from pathlib import Path import google.generativeai as genai from gtts import gTTS import tempfile import requests from PIL import Image, ImageDraw, ImageFont from io import BytesIO import re import time # Unsplash API Access Key UNSPLASH_ACCESS_KEY = "XQfXt81ei1xMuDBhTK_WayKF0pE-pLdfXMAcbgkQb7s" # Set page configuration st.set_page_config( page_title="Personal Audio Tutor", page_icon="🎓", layout="wide" ) # Initialize session state variables if they don't exist if 'explanation' not in st.session_state: st.session_state.explanation = "" if 'summary' not in st.session_state: st.session_state.summary = "" if 'notes' not in st.session_state: st.session_state.notes = "" if 'audio_file' not in st.session_state: st.session_state.audio_file = None if 'images' not in st.session_state: st.session_state.images = [] if 'gemini_model' not in st.session_state: st.session_state.gemini_model = None if 'api_configured' not in st.session_state: st.session_state.api_configured = False if 'use_unsplash' not in st.session_state: st.session_state.use_unsplash = False # Function to setup Gemini API def setup_gemini(api_key): try: genai.configure(api_key=api_key) # Initialize models once and store in session state st.session_state.gemini_model = genai.GenerativeModel('gemini-2.0-flash') # For image generation, use gemini-pro-vision return True except Exception as e: st.error(f"Error setting up Gemini API: {e}") return False def get_explanation(topic, detail_level="medium"): try: prompt = f""" As an expert educator, create a comprehensive explanation about '{topic}' at a {detail_level} level of detail. Break down the {topic} in parts and then explain the parts of the {topic} in detail step by step Explain core concepts ,principles details, examples, and applications in most simple langauge as you you are teaching to new born Include{detail_level}-level """ response = st.session_state.gemini_model.generate_content(prompt) return response.text except Exception as e: st.error(f"Error getting explanation: {e}") return "" def generate_study_notes(topic, explanation): try: prompt = f""" As an expert educator, transform this explanation about '{topic}' into comprehensive, structured study notes{explanation} Create organized study notes with the following components: Include all important terminology with clear, concise definitions Show practical applications of theoretical concepts Present key formulas, rules, or principles in a highlighted format Include brief explanations of when/how/why Provide concrete examples that illustrate key concepts Format the notes with clear headings, subheadings, bullet points, and numbering. Make the organization visually clear and easy to follow for effective studying. Focus on creating notes that would be valuable for review and reinforcement of learning. """ response = st.session_state.gemini_model.generate_content(prompt) return response.text except Exception as e: st.error(f"Error generating study notes: {e}") return "" def generate_summary(topic, explanation): try: prompt = f""" As an expert educator, create a concise, essential summary of this explanation about '{topic}': {explanation} Structure your summary as follows: 1. CORE CONCEPT (1-2 sentences) - Provide a clear, concise definition of what '{topic}' is 2. KEY POINTS (5-7 bullet points) - Extract the most crucial information and main concepts - Focus on what a student absolutely must understand about this topic - Ensure each point is distinct and captures a separate important idea - Keep each bullet point brief but informative (1-2 lines maximum) Format the summary with clean, consistent formatting and clear organization. The goal is to create a quick-reference guide that captures the essential knowledge in a highly digestible format. """ response = st.session_state.gemini_model.generate_content(prompt) return response.text except Exception as e: st.error(f"Error generating summary: {e}") return "" def generate_image_descriptions(topic, explanation, num_images=3): try: prompt = f""" As an educational visualization expert, create {num_images} detailed descriptions for educational diagrams about '{topic}' based on this explanation: {explanation} For each image description: 1. FOCUS ON A SINGLE KEY CONCEPT - Choose the most important, visually-explainable concepts from the topic - Select concepts that benefit from visual representation (processes, relationships, comparisons, structures) 2. PROVIDE DETAILED VISUALIZATION GUIDANCE - Describe the specific elements that should appear in the diagram - Specify relationships, connections, or flow between elements - Suggest visual organization (hierarchy, process flow, comparison, etc.) - Include labels, annotations, or callouts that should appear 3. EMPHASIZE EDUCATIONAL CLARITY - Focus on how the visualization will enhance understanding - Ensure the description would result in a diagram that simplifies complex ideas - Consider cognitive load and visual simplicity Format your response as a numbered list with only the descriptions, one per image. Each description should be detailed enough to create an effective educational diagram. """ response = st.session_state.gemini_model.generate_content(prompt) # Extract image descriptions (same as your current code) descriptions_text = response.text descriptions = [] # Simple parsing of numbered items pattern = r'\d+\.\s+(.*?)(?=\d+\.|$)' matches = re.findall(pattern, descriptions_text, re.DOTALL) if matches: descriptions = [match.strip() for match in matches] else: # Fallback: just split by lines and filter lines = [line.strip() for line in descriptions_text.split('\n') if line.strip()] descriptions = [line.split('. ', 1)[1] if '. ' in line else line for line in lines] return descriptions[:num_images] # Return only the requested number except Exception as e: st.error(f"Error generating image descriptions: {e}") return [] # Function to generate placeholder images with text overlay def generate_placeholder_images(image_descriptions): images = [] for i, description in enumerate(image_descriptions): try: # Create a placeholder image with the topic text width, height = 800, 600 img = Image.new('RGB', (width, height), color=(240, 248, 255)) # Light blue background # Add topic text as an overlay draw = ImageDraw.Draw(img) # Try to load a font, use default if not available try: font = ImageFont.truetype("Arial.ttf", 28) small_font = ImageFont.truetype("Arial.ttf", 20) except IOError: font = ImageFont.load_default() small_font = ImageFont.load_default() # Add a title at the top title = f"Concept {i + 1}" draw.text((width // 2, 50), title, fill=(0, 0, 128), font=font) # Wrap text to fit in the image words = description.split() lines = [] current_line = [] for word in words: current_line.append(word) if len(' '.join(current_line)) > 40: # Adjust based on your needs lines.append(' '.join(current_line[:-1])) current_line = [word] if current_line: lines.append(' '.join(current_line)) # Draw the wrapped text y_position = 150 for line in lines: text_width = draw.textlength(line, font=small_font) draw.text((width // 2 - text_width // 2, y_position), line, fill=(0, 0, 0), font=small_font) y_position += 30 # Draw a border draw.rectangle([(20, 20), (width - 20, height - 20)], outline=(0, 0, 128), width=2) images.append(img) except Exception as e: st.error(f"Error creating placeholder image {i + 1}: {e}") # Create a very simple fallback image with error message img = Image.new('RGB', (800, 600), color=(255, 240, 240)) # Light red background draw = ImageDraw.Draw(img) draw.text((400, 300), f"Error creating image: {str(e)}", fill=(128, 0, 0)) images.append(img) return images # New function to fetch images from Unsplash API def fetch_unsplash_images(search_terms, num_images=3): images = [] for i, term in enumerate(search_terms[:num_images]): try: # Clean up the search term - take first 2-3 words to make the search more focused words = term.split() if len(words) > 3: search_query = " ".join(words[:3]) else: search_query = term url = f"https://api.unsplash.com/photos/random?query={search_query}&client_id={UNSPLASH_ACCESS_KEY}&orientation=landscape" response = requests.get(url) if response.status_code == 200: data = response.json() image_url = data["urls"]["regular"] # Get the image content image_response = requests.get(image_url) image_data = Image.open(BytesIO(image_response.content)) # Resize to a consistent size for display image_data = image_data.resize((800, 600), Image.LANCZOS) # Add a caption overlay at the bottom draw = ImageDraw.Draw(image_data) try: font = ImageFont.truetype("Arial.ttf", 20) except IOError: font = ImageFont.load_default() # Add a semi-transparent background for the caption draw.rectangle([(0, 550), (800, 600)], fill=(0, 0, 0, 128)) # Add caption text caption = f"Concept {i + 1}: {search_query}" draw.text((400, 575), caption, fill=(255, 255, 255), anchor="ms", font=font) # Add photo credit if "user" in data and "name" in data["user"]: credit = f"Photo by {data['user']['name']} on Unsplash" draw.text((10, 590), credit, fill=(200, 200, 200), font=font) images.append(image_data) else: st.warning(f"Failed to fetch image {i+1} from Unsplash: {response.status_code}") # Return a placeholder instead images.append(generate_placeholder_images([term])[0]) except Exception as e: st.error(f"Error fetching Unsplash image for '{term}': {e}") # Return a placeholder instead images.append(generate_placeholder_images([term])[0]) return images # Function to generate audio from text def generate_audio(text, voice='en-US', speed=1.0): try: with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as temp_audio: tts = gTTS(text=text, lang=voice[:2], slow=False) tts.save(temp_audio.name) return temp_audio.name except Exception as e: st.error(f"Error generating audio: {e}") return None # Function to get file download link def get_download_link(file_path, label): with open(file_path, "rb") as file: contents = file.read() b64 = base64.b64encode(contents).decode() href = f'{label}' return href # Function to save content as text file and provide download link def get_text_download_link(text, filename, label): b64 = base64.b64encode(text.encode()).decode() href = f'{label}' return href # Function to check if a string is a valid API key format def is_valid_api_key_format(api_key): # A very basic check - Google API keys are typically long alphanumeric strings return bool(api_key and len(api_key) > 20 and api_key.startswith("AIza")) # Function to split text into chunks for audio generation def split_text_into_chunks(text, max_length=4000): # Split text into paragraphs paragraphs = text.split('\n') chunks = [] current_chunk = "" for paragraph in paragraphs: # If adding this paragraph would exceed max_length, start a new chunk if len(current_chunk) + len(paragraph) > max_length: chunks.append(current_chunk) current_chunk = paragraph + '\n' else: current_chunk += paragraph + '\n' # Don't forget the last chunk if current_chunk: chunks.append(current_chunk) return chunks # Main app UI def main(): st.title("🎓 Personal Audio Tutor") st.write("Your AI-powered study companion: Learn any topic through text, audio, and visuals") # Sidebar for settings with st.sidebar: st.header("Settings") api_key ="AIzaSyBoEzi2YrxrGZ2WwqDRCDTG6rbdXTj9yMQ" if api_key and is_valid_api_key_format(api_key): if st.button("Connect API") or not st.session_state.api_configured: api_configured = setup_gemini(api_key) st.session_state.api_configured = api_configured if api_configured: st.success("API connected successfully!") if not st.session_state.api_configured: st.warning("Please enter a valid Gemini API key to continue") st.subheader("Audio Settings") voice_options = { 'en-US': 'English (US)', 'en-GB': 'English (UK)', 'fr-FR': 'French', 'de-DE': 'German', 'es-ES': 'Spanish', 'it-IT': 'Italian' } selected_voice = st.selectbox("Select Voice", list(voice_options.keys()), format_func=lambda x: voice_options[x]) speed = st.slider("Playback Speed", min_value=0.5, max_value=2.0, value=1.0, step=0.1) st.subheader("Content Settings") detail_level = st.radio("Explanation Detail Level", ["basic", "medium", "advanced"], index=1) num_images = st.slider("Number of Images", min_value=1, max_value=5, value=3) # Unsplash toggle st.session_state.use_unsplash = st.checkbox("Use Unsplash for real images", value=st.session_state.use_unsplash) if st.session_state.use_unsplash: st.info("Unsplash API will be used to generate real images based on the topic concepts.") # Export/import functionality st.subheader("Save/Load Session") if st.session_state.explanation: if st.button("Export Session Data"): session_data = { "explanation": st.session_state.explanation, "summary": st.session_state.summary, "notes": st.session_state.notes, "topic": st.session_state.get("current_topic", "") } b64 = base64.b64encode(str(session_data).encode()).decode() st.markdown( f'Download Session Data', unsafe_allow_html=True ) # Main input area st.header("What would you like to learn about today?") col1, col2 = st.columns([3, 1]) with col1: topic = st.text_input("Enter a topic, chapter, or book title:") with col2: topic_type = st.selectbox("Learning Type", ["Topic", "Chapter", "Book"], index=0) # Process button if st.button("Generate Learning Materials") and topic and st.session_state.api_configured: st.session_state.current_topic = topic with st.spinner("Generating your personalized learning materials..."): # Generate explanation st.session_state.explanation = get_explanation(topic, detail_level) if st.session_state.explanation: # Generate study notes and summary st.session_state.notes = generate_study_notes(topic, st.session_state.explanation) st.session_state.summary = generate_summary(topic, st.session_state.explanation) # Generate image descriptions image_descriptions = generate_image_descriptions(topic, st.session_state.explanation, num_images) st.session_state.image_descriptions = image_descriptions # Generate either placeholder images or Unsplash images based on user preference if st.session_state.use_unsplash: st.session_state.images = fetch_unsplash_images(image_descriptions, num_images) else: st.session_state.images = generate_placeholder_images(image_descriptions) # Generate audio text = st.session_state.explanation # FIXED: Renamed function call to avoid name collision text_chunks = split_text_into_chunks(text) if len(text_chunks) == 1: st.session_state.audio_file = generate_audio(text, selected_voice, speed) st.session_state.has_multiple_chunks = False else: # For longer text, only generate audio for the first chunk first_chunk = text_chunks[0] st.session_state.audio_file = generate_audio(first_chunk, selected_voice, speed) st.session_state.text_chunks = text_chunks st.session_state.has_multiple_chunks = True # Display results if available if st.session_state.explanation: # Use tabs to organize the different types of content tab1, tab2, tab3, tab4 = st.tabs(["Explanation", "Audio Narration", "Visual Aids", "Study Materials"]) with tab1: st.subheader(f"{topic_type}: {st.session_state.current_topic}") # Add a search function for the explanation search_term = st.text_input("Search in explanation:", key="search_explanation") if search_term: highlighted_text = st.session_state.explanation.replace( search_term, f"**{search_term}**" ) st.markdown(highlighted_text) else: st.markdown(st.session_state.explanation) with tab2: st.subheader("Audio Narration") if st.session_state.audio_file: st.audio(st.session_state.audio_file) st.markdown(get_download_link(st.session_state.audio_file, "Download Audio File"), unsafe_allow_html=True) # Show information about text chunking if applicable if st.session_state.has_multiple_chunks: st.info( f"The explanation has been split into {len(st.session_state.text_chunks)} parts for audio generation. Currently playing part 1.") # Add option to generate audio for other chunks chunk_options = [f"Part {i + 1}" for i in range(len(st.session_state.text_chunks))] selected_chunk_index = st.selectbox( "Select part to play:", range(len(chunk_options)), format_func=lambda x: chunk_options[x] ) if st.button("Generate Audio for Selected Part"): with st.spinner(f"Generating audio for {chunk_options[selected_chunk_index]}..."): chunk_text = st.session_state.text_chunks[selected_chunk_index] st.session_state.audio_file = generate_audio(chunk_text, selected_voice, speed) st.experimental_rerun() # Audio controls st.subheader("Audio Controls") if st.button("Regenerate Audio"): with st.spinner("Generating new audio..."): # If we have multiple chunks, just regenerate the current chunk if st.session_state.has_multiple_chunks: current_chunk_index = 0 # Default to first chunk if 'selected_chunk_index' in locals(): current_chunk_index = selected_chunk_index chunk_text = st.session_state.text_chunks[current_chunk_index] st.session_state.audio_file = generate_audio(chunk_text, selected_voice, speed) else: # Otherwise regenerate the full audio st.session_state.audio_file = generate_audio(st.session_state.explanation, selected_voice, speed) st.experimental_rerun() with tab3: st.subheader("Visual Aids") # Display image descriptions if hasattr(st.session_state, 'image_descriptions'): for i, desc in enumerate(st.session_state.image_descriptions): st.markdown(f"**Image {i + 1}**: {desc}") # Toggle for Unsplash images col1, col2 = st.columns([3, 1]) with col2: use_unsplash_toggle = st.checkbox("Use Unsplash Images", value=st.session_state.use_unsplash, key="toggle_unsplash") if use_unsplash_toggle != st.session_state.use_unsplash: st.session_state.use_unsplash = use_unsplash_toggle # Regenerate images based on the new setting with st.spinner("Updating images..."): if st.session_state.use_unsplash: st.session_state.images = fetch_unsplash_images(st.session_state.image_descriptions, len(st.session_state.image_descriptions)) else: st.session_state.images = generate_placeholder_images(st.session_state.image_descriptions) st.experimental_rerun() # Display images in a grid if st.session_state.images: cols = st.columns(min(3, len(st.session_state.images))) for i, img in enumerate(st.session_state.images): col_idx = i % 3 with cols[col_idx]: st.image(img, use_column_width=True, caption=f"Concept {i + 1}") # Add individual image regeneration buttons if st.button(f"Regenerate Image {i+1}"): with st.spinner(f"Regenerating image {i+1}..."): if st.session_state.use_unsplash: # Fetch a new image from Unsplash for this concept search_term = st.session_state.image_descriptions[i] new_images = fetch_unsplash_images([search_term], 1) if new_images: st.session_state.images[i] = new_images[0] else: # Generate a new placeholder image new_images = generate_placeholder_images([st.session_state.image_descriptions[i]]) if new_images: st.session_state.images[i] = new_images[0] st.experimental_rerun() # Add button to regenerate all visuals if st.button("Regenerate All Visual Aids"): with st.spinner("Creating new visuals..."): image_descriptions = generate_image_descriptions( st.session_state.current_topic, st.session_state.explanation, num_images ) st.session_state.image_descriptions = image_descriptions # Generate the appropriate type of images if st.session_state.use_unsplash: st.session_state.images = fetch_unsplash_images(image_descriptions, len(image_descriptions)) else: st.session_state.images = generate_placeholder_images(image_descriptions) st.experimental_rerun() with tab4: st.subheader("Study Materials") sub_tab1, sub_tab2, sub_tab3 = st.tabs(["Structured Notes", "Bullet-Point Summary", "Quiz"]) with sub_tab1: st.markdown(st.session_state.notes) st.markdown(get_text_download_link( st.session_state.notes, f"{st.session_state.current_topic.replace(' ', '_')}_notes.txt", "Download Notes"), unsafe_allow_html=True) with sub_tab2: st.markdown(st.session_state.summary) st.markdown( get_text_download_link( st.session_state.summary, f"{st.session_state.current_topic.replace(' ', '_')}_summary.txt", "Download Summary"), unsafe_allow_html=True) with sub_tab3: # Generate a quiz on demand if st.button("Generate Quiz"): with st.spinner("Creating quiz questions..."): try: prompt = f""" Based on this explanation about '{st.session_state.current_topic}': {st.session_state.explanation} Create 5 multiple-choice quiz questions to test understanding of key concepts. For each question, provide 4 options and indicate the correct answer. Format as: Q1: [Question] A. [Option A] B. [Option B] C. [Option C] D. [Option D] Correct Answer: [Letter] Then repeat for Q2 through Q5. """ response = st.session_state.gemini_model.generate_content(prompt) st.session_state.quiz = response.text except Exception as e: st.error(f"Error generating quiz: {e}") st.session_state.quiz = "Failed to generate quiz. Please try again." # Display quiz if available if 'quiz' in st.session_state and st.session_state.quiz: st.markdown(st.session_state.quiz) st.markdown( get_text_download_link( st.session_state.quiz, f"{st.session_state.current_topic.replace(' ', '_')}_quiz.txt", "Download Quiz"), unsafe_allow_html=True ) else: st.info("Click 'Generate Quiz' to create interactive questions about this topic") # Run the app if __name__ == "__main__": main()