|
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 |
|
|
|
|
|
st.set_page_config( |
|
page_title="Interactive Course Preview Generator", |
|
layout="wide", |
|
initial_sidebar_state="expanded" |
|
) |
|
|
|
|
|
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) |
|
|
|
|
|
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.""" |
|
|
|
|
|
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 |
|
} |
|
) |
|
|
|
|
|
for _ in range(30): |
|
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..."): |
|
|
|
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) |
|
|
|
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: |
|
|
|
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 |
|
|
|
|
|
sections = parse_course_content(course_content) |
|
preview_concept = sections[0]['concepts'][0] if sections and sections[0].get('concepts') else topic |
|
|
|
|
|
preview_response = client.messages.create( |
|
model="claude-3-5-sonnet-latest", |
|
max_tokens=4096, |
|
messages=[ |
|
{ |
|
"role": "user", |
|
"content": PREVIEW_TEMPLATE.format(concept=preview_concept) |
|
} |
|
] |
|
) |
|
|
|
|
|
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 |
|
|
|
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:'): |
|
|
|
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}") |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
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: |
|
|
|
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) |
|
|
|
|
|
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: |
|
|
|
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") |
|
|
|
|
|
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: |
|
|
|
outline_result = generate_course_outline(topic, level, duration) |
|
if outline_result["status"] != "success": |
|
st.error(f"Error generating outline: {outline_result.get('message')}") |
|
return |
|
|
|
|
|
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" |
|
) |
|
|
|
|
|
preview_content = generate_preview_content(topic, outline_result["first_concept"]) |
|
if not preview_content: |
|
st.error("Failed to generate preview content") |
|
return |
|
|
|
|
|
slides = parse_preview_content(preview_content) |
|
|
|
|
|
|
|
|
|
st.markdown("## Course Overview") |
|
st.markdown(outline_result["outline"]) |
|
|
|
|
|
st.markdown("## Instructor Introduction") |
|
if intro_audio: |
|
st.audio(intro_audio) |
|
|
|
|
|
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() |