Personal_tutor / app.py
pranit144's picture
Update app.py
6129e63 verified
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()