oussnaji's picture
Create app.py
bab012b verified
import streamlit as st
import anthropic
import requests
import base64
import json
import time
import os
from typing import Dict, List, Any
import fal_client
from dotenv import load_dotenv
from IPython.display import Image as IPImage, display
# Page config
st.set_page_config(
page_title="Interactive Course Preview Generator",
layout="wide",
initial_sidebar_state="expanded"
)
# Custom CSS for modern, minimalist design
st.markdown("""
<style>
.main {background-color: #f8f9fa;}
.stButton>button {
background-color: #4c6ef5;
color: white;
border-radius: 4px;
border: none;
padding: 0.5rem 1rem;
}
.content-box {
background-color: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin: 1rem 0;
}
.section-title {
color: #4c6ef5;
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 1rem;
}
</style>
""", unsafe_allow_html=True)
st.markdown("""
<style>
.main {background-color: #f8f9fa;}
.subtitle {
color: #6c757d;
font-size: 0.9rem;
font-style: italic;
margin-top: 0.5rem;
}
.preview-content {
background-color: #f8f9fa;
padding: 1rem;
border-left: 3px solid #4c6ef5;
margin: 1rem 0;
}
.script-content {
background-color: #e9ecef;
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
}
</style>
""", unsafe_allow_html=True)
# Templates for content generation
COURSE_TEMPLATE = """Create a concise course outline for:
Topic: {topic}
Level: {level}
Duration: {duration}
Include:
1. Brief course description (2-3 sentences)
2. 4-5 main sections
3. For each section:
- Title
- Brief description (1 sentence)
- 2-3 key concepts
Format it clearly and professionally."""
PREVIEW_TEMPLATE = """Create content for two brief preview slides about the concept: {concept}
For each slide include EXACTLY in this order:
1. Slide Title
2. Content (3 brief bullet points maximum)
3. Teacher Script (2-3 sentences maximum)
4. Image Description (one sentence describing a minimalist visual)
Format each slide clearly with these exact headers:
"Slide 1:", "Content:", "Teacher Script:", "Image Description:"
"Slide 2:", "Content:", "Teacher Script:", "Image Description:"
"""
INSTRUCTOR_INTRO_TEMPLATE = """Create a very brief welcome message (2 sentences maximum) for:
Instructor: {name}
Course: {topic}
Style: {style}
Keep it natural and concise."""
# API Integration Functions
def generate_image(description: str) -> str:
"""Generate image using verified Fal.ai implementation with proper waiting"""
try:
with st.spinner(f"Generating image for: {description}"):
handler = fal_client.submit(
"fal-ai/flux-pro/v1.1-ultra",
arguments={
"prompt": f"Professional minimalist educational visual: {description}",
"num_images": 1
}
)
# Extended wait time with status check
for _ in range(30): # Maximum 30 seconds wait
time.sleep(1)
result = fal_client.result("fal-ai/flux-pro/v1.1-ultra", handler.request_id)
if result and "images" in result and result["images"]:
return result["images"][0]["url"]
return None
except Exception as e:
st.error(f"Image generation error: {e}")
return None
def generate_and_get_image(prompt: str) -> str:
"""Generate and get image using verified Fal.ai implementation"""
try:
with st.spinner(f"Generating image..."):
# Submit request
handler = fal_client.submit(
"fal-ai/flux-pro/v1.1-ultra",
arguments={"prompt": prompt, "num_images": 1}
)
request_id = handler.request_id
if request_id:
time.sleep(10) # Wait for generation
# Get result
result = fal_client.result("fal-ai/flux-pro/v1.1-ultra", request_id)
if result and "images" in result and result["images"]:
return result["images"][0]["url"]
except Exception as e:
st.error(f"Image generation error: {e}")
return None
def create_voice_preview(text: str, voice_description: str) -> bytes:
"""Create voice preview using verified ElevenLabs implementation"""
url = "https://api.elevenlabs.io/v1/text-to-voice/create-previews"
headers = {
'xi-api-key': st.secrets["ELEVENLABS_API_KEY"],
'Content-Type': 'application/json'
}
payload = {
'voice_description': voice_description,
'text': text
}
try:
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
return base64.b64decode(response.json()['previews'][0]['audio_base_64'])
return None
except Exception as e:
st.error(f"Voice generation error: {e}")
return None
def generate_content(topic: str, level: str, duration: str,
instructor_name: str, teaching_style: str) -> Dict:
"""Generate course content using Claude 3.5"""
client = anthropic.Anthropic(api_key=st.secrets["ANTHROPIC_API_KEY"])
try:
# Generate course outline
outline_response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=4096,
messages=[
{
"role": "user",
"content": COURSE_TEMPLATE.format(
topic=topic,
level=level,
duration=duration
)
}
]
)
course_content = outline_response.content[0].text
# Parse content to get a concept for preview
sections = parse_course_content(course_content)
preview_concept = sections[0]['concepts'][0] if sections and sections[0].get('concepts') else topic
# Generate preview content
preview_response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=4096,
messages=[
{
"role": "user",
"content": PREVIEW_TEMPLATE.format(concept=preview_concept)
}
]
)
# Generate instructor introduction
intro_response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=4096,
messages=[
{
"role": "user",
"content": INSTRUCTOR_INTRO_TEMPLATE.format(
name=instructor_name,
style=teaching_style,
topic=topic
)
}
]
)
return {
"status": "success",
"course_outline": course_content,
"preview_content": preview_response.content[0].text,
"instructor_intro": intro_response.content[0].text,
"sections": sections
}
except Exception as e:
return {"status": "error", "message": str(e)}
def parse_outline(content: str) -> List[Dict]:
"""Parse course outline to get sections and concepts"""
sections = []
current_section = None
for line in content.split('\n'):
line = line.strip()
if not line:
continue
if line.lower().startswith(('section', 'part', 'module')):
if current_section:
sections.append(current_section)
current_section = {
'title': line,
'description': '',
'concepts': []
}
elif current_section:
if not current_section['description']:
current_section['description'] = line
elif line.startswith(('-', '•', '*')):
current_section['concepts'].append(line.lstrip('-•* '))
if current_section:
sections.append(current_section)
return sections
def parse_course_content(content: str) -> List[Dict]:
"""Parse course content into structured format"""
sections = []
current_section = None
for line in content.split('\n'):
line = line.strip()
if not line:
continue
if line.lower().startswith(('section', 'part', 'module')):
if current_section:
sections.append(current_section)
current_section = {
'title': line,
'description': '',
'concepts': []
}
elif current_section:
if not current_section['description']:
current_section['description'] = line
elif line.startswith(('-', '•', '*')):
current_section['concepts'].append(line.lstrip('-•* '))
if current_section:
sections.append(current_section)
return sections
def generate_course_outline(topic: str, level: str, duration: str) -> Dict:
"""Generate initial course outline and get first concept"""
client = anthropic.Anthropic(api_key=st.secrets["ANTHROPIC_API_KEY"])
prompt = f"""Create a course outline for:
Topic: {topic}
Level: {level}
Duration: {duration}
Include:
1. Brief course description (2-3 sentences)
2. 4-5 main sections with:
- Clear title
- 2-3 key concepts per section
- Brief description
Format clearly with sections and concepts."""
try:
response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}]
)
outline = response.content[0].text
# Parse to get first concept
sections = parse_outline(outline)
first_concept = sections[0]['concepts'][0] if sections and sections[0].get('concepts') else topic
return {
"status": "success",
"outline": outline,
"sections": sections,
"first_concept": first_concept
}
except Exception as e:
return {"status": "error", "message": str(e)}
def generate_preview_content(topic: str, concept: str) -> str:
"""Generate preview content using Claude"""
client = anthropic.Anthropic(api_key=st.secrets["ANTHROPIC_API_KEY"])
prompt = f"""Create TWO preview slides about {concept} for a course on {topic}.
For EACH slide, provide EXACTLY in this format:
SLIDE [number]:
- Title: [slide title]
- Content: [3 clear bullet points]
- Teaching Script: [2-3 sentences explaining the slide content]
- Visual Description: [clear description for image generation]
Keep all content clear and concise."""
try:
response = client.messages.create(
model="claude-3-5-sonnet-latest",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text
except Exception as e:
st.error(f"Content generation error: {e}")
return None
def parse_preview_content(content: str) -> List[Dict]:
"""Parse preview content into structured format"""
slides = []
current_slide = None
for line in content.split('\n'):
line = line.strip()
if not line:
continue
if line.startswith('SLIDE'):
if current_slide:
slides.append(current_slide)
current_slide = {
'title': '',
'content': [],
'script': '',
'visual': ''
}
elif current_slide:
if line.startswith('- Title:'):
current_slide['title'] = line.replace('- Title:', '').strip()
elif line.startswith('- Content:'):
# Next lines will be content until next section
continue
elif line.startswith('- Teaching Script:'):
current_slide['script'] = line.replace('- Teaching Script:', '').strip()
elif line.startswith('- Visual Description:'):
current_slide['visual'] = line.replace('- Visual Description:', '').strip()
elif line.startswith('-') or line.startswith('•'):
current_slide['content'].append(line.lstrip('-• ').strip())
if current_slide:
slides.append(current_slide)
return slides
def display_preview_content(preview_slides: List[Dict]):
"""Display preview content with proper styling"""
for i, slide in enumerate(preview_slides, 1):
st.markdown(f"### Preview Slide {i}")
# Content section
st.markdown('<div class="preview-content">', unsafe_allow_html=True)
for point in slide['content']:
st.markdown(f"• {point}")
st.markdown('</div>', unsafe_allow_html=True)
# Script section
st.markdown('<div class="script-content">', unsafe_allow_html=True)
st.markdown("**Teaching Script:**")
st.markdown(slide['script'])
st.markdown('</div>', unsafe_allow_html=True)
# Generate and display image
if slide['image_description']:
image_url = generate_image(slide['image_description'])
if image_url:
st.image(image_url, use_column_width=True)
def display_preview_slides(slides: List[Dict]):
"""Display preview slides with proper styling"""
for i, slide in enumerate(slides, 1):
st.markdown(f"### {slide['title']}")
col1, col2 = st.columns([2, 1])
with col1:
# Content
st.markdown('<div class="preview-content">', unsafe_allow_html=True)
for point in slide['content']:
st.markdown(f"• {point}")
st.markdown('</div>', unsafe_allow_html=True)
# Teaching Script
st.markdown('<div class="script-content">', unsafe_allow_html=True)
st.markdown("**Teaching Script:**")
st.markdown(slide['script'])
st.markdown('</div>', unsafe_allow_html=True)
with col2:
# Generate and display image
if slide['visual']:
image_url = generate_and_get_image(slide['visual'])
if image_url:
st.image(image_url, use_column_width=True)
def main():
st.title("Interactive Course Preview Generator")
st.markdown("Generate professional course previews with content, visuals, and voice narration")
# Input Section
with st.container():
st.markdown('<div class="section-title">Course Configuration</div>', unsafe_allow_html=True)
col1, col2 = st.columns(2)
with col1:
topic = st.text_input("Course Topic",
placeholder="e.g., Machine Learning Fundamentals")
level = st.selectbox("Course Level",
["Beginner", "Intermediate", "Advanced"])
duration = st.selectbox("Course Duration",
["2 Hours", "4 Hours", "8 Hours", "Full Day"])
with col2:
instructor_name = st.text_input("Instructor Name",
placeholder="e.g., Dr. Sarah Johnson")
teaching_style = st.selectbox("Teaching Style",
["Interactive", "Lecture-Based", "Project-Based", "Discussion-Led"])
instructor_gender = st.selectbox("Instructor Voice",
["Male", "Female"])
if st.button("Generate Preview", type="primary"):
with st.spinner("Creating your course preview..."):
try:
# Step 1: Generate course outline and get first concept
outline_result = generate_course_outline(topic, level, duration)
if outline_result["status"] != "success":
st.error(f"Error generating outline: {outline_result.get('message')}")
return
# Step 2: Generate instructor introduction
intro_audio = create_voice_preview(
f"Hello! I'm {instructor_name}, and I'll be your instructor for {topic}. Let's explore this exciting subject together!",
f"Professional {instructor_gender.lower()} instructor, {teaching_style.lower()} style"
)
# Step 3: Generate preview content using first concept
preview_content = generate_preview_content(topic, outline_result["first_concept"])
if not preview_content:
st.error("Failed to generate preview content")
return
# Step 4: Parse and display content
slides = parse_preview_content(preview_content)
# Display results
# Course Outline
st.markdown("## Course Overview")
st.markdown(outline_result["outline"])
# Instructor Introduction
st.markdown("## Instructor Introduction")
if intro_audio:
st.audio(intro_audio)
# Preview Slides
st.markdown("## Preview Slides")
display_preview_slides(slides)
except Exception as e:
st.error(f"An error occurred: {str(e)}")
return
if __name__ == "__main__":
main()