Spaces:
Build error
Build error
| 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'<a href="data:audio/mp3;base64,{b64}" download="{os.path.basename(file_path)}">{label}</a>' | |
| 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'<a href="data:file/txt;base64,{b64}" download="{filename}">{label}</a>' | |
| 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'<a href="data:file/txt;base64,{b64}" download="tutor_session.txt">Download Session Data</a>', | |
| 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() |