jeremierostan's picture
Update app.py
6de658f verified
import gradio as gr
import PyPDF2
from pptx import Presentation
from PIL import Image
import io
import google.generativeai as genai
import fitz # PyMuPDF
import os
import logging
import re
import time
import hashlib
# Set up logging
logging.basicConfig(level=logging.INFO)
# Get secrets from environment variables
APP_PASSWORD = os.environ.get("APP_PASSWORD") # Default fallback for testing
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
# ISP Brand Colors - Elegant Blue Palette
ISP_COLORS = {
"primary": "#1e3a5f", # Deep midnight blue
"secondary": "#4a7ba7", # Steel blue
"accent": "#6fa3d2", # Sky blue
"light": "#a8c9e5", # Powder blue
"success": "#2e86ab", # Ocean blue
"background": "#f0f4f8", # Ice blue
"text": "#2c3e50" # Charcoal blue
}
# Custom CSS for ISP branding - Elegant Blue Theme
custom_css = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
.gradio-container {
font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif !important;
background: linear-gradient(135deg, #f0f4f8 0%, #e1ecf4 100%) !important;
}
.gr-button-primary {
background: linear-gradient(135deg, #1e3a5f 0%, #4a7ba7 100%) !important;
border: none !important;
color: white !important;
font-weight: 500 !important;
letter-spacing: 0.5px !important;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important;
box-shadow: 0 4px 14px rgba(30, 58, 95, 0.25) !important;
}
.gr-button-primary:hover {
transform: translateY(-2px) !important;
box-shadow: 0 8px 24px rgba(30, 58, 95, 0.35) !important;
background: linear-gradient(135deg, #2e4a6f 0%, #5a8bb7 100%) !important;
}
.gr-button-secondary {
background: rgba(255, 255, 255, 0.95) !important;
border: 2px solid #4a7ba7 !important;
color: #1e3a5f !important;
font-weight: 500 !important;
transition: all 0.3s ease !important;
}
.gr-button-secondary:hover {
background: #f0f4f8 !important;
border-color: #1e3a5f !important;
box-shadow: 0 3px 10px rgba(30, 58, 95, 0.15) !important;
}
h1 {
color: #1e3a5f !important;
font-weight: 700 !important;
letter-spacing: -0.5px !important;
text-shadow: 0 2px 4px rgba(30, 58, 95, 0.1) !important;
}
h2, h3 {
color: #2e4a6f !important;
font-weight: 600 !important;
}
.login-container {
max-width: 400px;
margin: 100px auto;
padding: 40px;
background: linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(240,244,248,0.95) 100%);
border-radius: 20px;
box-shadow: 0 20px 60px rgba(30, 58, 95, 0.15);
border: 1px solid rgba(168, 201, 229, 0.3);
}
.header-banner {
background: linear-gradient(135deg, #1e3a5f 0%, #4a7ba7 50%, #6fa3d2 100%);
color: white;
padding: 30px;
border-radius: 16px;
margin-bottom: 30px;
text-align: center;
box-shadow: 0 10px 30px rgba(30, 58, 95, 0.25);
position: relative;
overflow: hidden;
}
.header-banner::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: shimmer 15s linear infinite;
}
@keyframes shimmer {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.success-notification {
background: linear-gradient(135deg, #2e86ab 0%, #6fa3d2 100%);
color: white;
padding: 14px 24px;
border-radius: 10px;
margin: 10px 0;
animation: slideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 6px 20px rgba(46, 134, 171, 0.3);
}
@keyframes slideIn {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.isp-footer {
text-align: center;
padding: 25px;
color: #4a7ba7;
font-size: 14px;
border-top: 2px solid #e1ecf4;
margin-top: 50px;
background: linear-gradient(180deg, transparent 0%, rgba(240,244,248,0.5) 100%);
}
.gr-file {
border: 2px dashed #6fa3d2 !important;
background: rgba(111, 163, 210, 0.05) !important;
transition: all 0.3s ease !important;
}
.gr-file:hover {
border-color: #4a7ba7 !important;
background: rgba(74, 123, 167, 0.08) !important;
}
.gr-textbox input, .gr-textbox textarea {
border: 2px solid #a8c9e5 !important;
background: white !important;
transition: all 0.3s ease !important;
}
.gr-textbox input:focus, .gr-textbox textarea:focus {
border-color: #4a7ba7 !important;
box-shadow: 0 0 0 3px rgba(74, 123, 167, 0.1) !important;
}
.password-input input {
font-size: 16px !important;
padding: 12px !important;
}
.gr-markdown {
color: #2c3e50 !important;
line-height: 1.6 !important;
}
.gr-box {
border-radius: 12px !important;
border: 1px solid rgba(168, 201, 229, 0.2) !important;
background: rgba(255, 255, 255, 0.8) !important;
}
"""
# Password verification function
def verify_password(password):
"""Verify if the entered password matches the app password"""
if not APP_PASSWORD:
return True, "⚠️ No password set in environment. Access granted."
if password == APP_PASSWORD:
return True, "✅ Access granted! Welcome to ISP Reverse Unit Planner."
else:
return False, "❌ Incorrect password. Please try again."
# Function to extract content from PDF
def extract_content_from_pdf(file_path):
try:
text = ""
images = []
doc = fitz.open(file_path)
for page_num, page in enumerate(doc):
text += f"\n--- Page {page_num + 1} ---\n"
text += page.get_text() + "\n"
for img_index, img in enumerate(page.get_images()):
try:
xref = img[0]
base_image = doc.extract_image(xref)
image_bytes = base_image["image"]
image = Image.open(io.BytesIO(image_bytes))
images.append(image)
except Exception as img_error:
logging.warning(f"Could not extract image {img_index} from page {page_num + 1}: {img_error}")
doc.close()
return text, images
except Exception as e:
logging.error(f"Error extracting content from PDF: {e}")
return "", []
# Function to extract content from PPTX
def extract_content_from_pptx(file_path):
try:
text = ""
images = []
prs = Presentation(file_path)
for slide_num, slide in enumerate(prs.slides):
text += f"\n--- Slide {slide_num + 1} ---\n"
for shape in slide.shapes:
if hasattr(shape, 'text') and shape.text:
text += shape.text + "\n"
if shape.shape_type == 13: # Picture
try:
image = shape.image
image_bytes = image.blob
img = Image.open(io.BytesIO(image_bytes))
images.append(img)
except Exception as img_error:
logging.warning(f"Could not extract image from slide {slide_num + 1}: {img_error}")
return text, images
except Exception as e:
logging.error(f"Error extracting content from PPTX: {e}")
return "", []
# Function to process file
def process_file(file_path):
if file_path is None:
return "No file uploaded", []
try:
file_ext = file_path.lower().split('.')[-1]
if file_ext == 'pdf':
return extract_content_from_pdf(file_path)
elif file_ext in ['pptx', 'ppt']:
return extract_content_from_pptx(file_path)
else:
return f"Unsupported file format: .{file_ext}", []
except Exception as e:
logging.error(f"Error processing file: {e}")
return f"An error occurred while processing the file: {str(e)}", []
# -----------------------
# Math/Science rendering helpers (NEW)
# -----------------------
_inline_guard = r"[\\^_{}]" # cue that $...$ is likely LaTeX, not currency
def _normalize_math_delimiters(s: str) -> str:
"""Convert $$..$$ -> \\[..\\] and $..$ -> \\(..\\) while avoiding $20 currency."""
if not s:
return s
# Display math $$...$$ (multiline)
s = re.sub(r"\$\$(.+?)\$\$", r"[[DISPLAY_MATH:\1]]", s, flags=re.DOTALL)
# Inline math $...$ (convert only if it looks like LaTeX)
def _inline_repl(m):
inner = m.group(1)
# If inner contains typical LaTeX tokens, treat as math; else leave alone
if re.search(_inline_guard, inner):
return f"((INLINE_MATH:{inner}))"
return f"${inner}$"
s = re.sub(r"\$(.+?)\$", _inline_repl, s, flags=re.DOTALL)
# Finalize placeholders
s = s.replace("[[DISPLAY_MATH:", r"\[").replace("]]", r"\]")
s = s.replace("((INLINE_MATH:", r"\(").replace("))", r"\)")
return s
# Function to clean response
def clean_response(response_text):
# Remove code block markers if present
cleaned = re.sub(r'```python|```markdown|```', '', response_text).strip()
# Handle newlines and indentation
cleaned = re.sub(r'\n\s*\n\s*\n+', '\n\n', cleaned)
# Normalize math delimiters for reliable rendering
cleaned = _normalize_math_delimiters(cleaned)
return cleaned
# Function to understand text and images using Gemini V2 API
def understand_content(text, images, progress=gr.Progress()):
try:
if not GEMINI_API_KEY:
return "❌ Gemini API key not configured. Please contact the administrator."
# Configure the Gemini API
genai.configure(api_key=GEMINI_API_KEY)
progress(0.3, desc="🔍 Analyzing content...")
# Prepare content for Gemini
content_parts = [text]
# Add images if available (limit to 5 for performance)
for i, image in enumerate(images[:5]):
if i == 0:
progress(0.4, desc="🖼️ Processing images...")
content_parts.append(image)
progress(0.5, desc="🎯 Generating unit plan...")
# Generate response from Gemini
prompt = """
You are an expert instructional designer at International School of Panama (ISP).
Below are materials shared by a teacher. Your role is to reverse-engineer a comprehensive unit planner for this content.
Please create a unit planner that follows this EXACT structure:
# 📚 UNIT PLANNER
## 📋 Standards
- If the subject is English, use Common Core
- If the subject is Spanish, use Common Core
- If the subject is English or Spanish Acquisition (EAL, SAL), use ACTFL
- If the subject is Social Studies, use AERO
- If the subject is Mathematics, use AERO
- If the subject is Science, use NGSS
## 🎯 Transfer Goal
(This answers: "Why are we learning this?" How will students transfer their learning in "real-life" and in the future?)
## ❓ Essential Questions
(What are the key questions that students will be asking, exploring, and answering through this unit?)
## 💡 Enduring Understandings
(This is the "U" in KUD and helps students answer the essential questions by connecting concepts. Students will understand that [connections between key concepts] and why [connections between key concepts])
## 📖 Students Will Know
(This is the "K" in KUD, the content that students will use and process to construct concepts - the building blocks of understandings. Students will know [fact, formula, definition…])
## 🛠️ Students Will Be Able To
(This is the "D" in KUD, the skills that students will develop and use to process knowledge, concepts, understandings, and answer the essential questions. These skills should be connected to the command terms.)
## 📝 Formative Assessments
## 📊 Summative Assessments
## 🗓️ Scope and Sequence
(This is a description of the overall flow of the unit. It includes an assessment plan and brief lesson objectives along the learning scale and towards the summative)
## 🌟 Unit Overview
(This is a brief summation of everything above. What will students be learning, and why? How will they progressively develop and demonstrate their learning?)
## ⚠️ Potential Barriers
(This identifies potential barriers to student learning and strategies to address them)
## 🔗 Connections
### ISP Core Values
- **Commitment to Excellence**:
- **Strength in Diversity**:
- **Compassion and Integrity**:
- **Innovative Spirit**:
- **Lasting Impact**:
### IB Theory of Knowledge (TOK)
### IB Approaches to Learning (ATL)
- Collaboration skills:
- Communication skills:
- Affective skills:
- Reflection skills:
- Information and media literacy skills:
- Critical thinking skills:
- Creative thinking skills:
## 🎭 Authentic Assessment (GRASPS)
**Goal**: Assign an authentic (real-life), exciting (challenging), and meaningful (relatable, impactful) project or problem to solve
**Role**: Give students an authentic, exciting, and meaningful role to play
**Audience**: Identify an authentic, exciting, and meaningful audience that students can serve
**Situation**: Create an authentic, exciting, and meaningful scenario or context
**Project/Product/Performance and Progress**: Clarify what students are expected to do and how
**Success Criteria**: Provide task-specific learning scales and benchmark sheets
---
*Unit planner generated 🎉*
Content to analyze:
"""
# Generate response from Gemini
model = genai.GenerativeModel('gemini-flash-latest')
response = model.generate_content(prompt + "\n\n" + text)
progress(0.8, desc="✨ Finalizing output...")
# Get and clean the response
response_text = response.text
cleaned_response = clean_response(response_text)
progress(1.0, desc="✅ Complete!")
return cleaned_response
except Exception as e:
logging.error(f"Error in content understanding: {e}")
return f"❌ Error in processing: {str(e)}"
# Function to reverse engineer unit plan
def generate_elt_plan(file, progress=gr.Progress()):
try:
progress(0.1, desc="📂 Opening file...")
if file is None:
return "Please upload a file first", None
logging.info(f"Processing file: {file.name}")
content, images = process_file(file.name)
if isinstance(content, str) and ("error occurred" in content.lower() or "unsupported" in content.lower()):
return content, None
if not content or len(content.strip()) < 50:
return "❌ The file appears to be empty or contains very little text. Please check the file and try again.", None
logging.info(f"Extracted content length: {len(content)}, Number of images: {len(images)}")
progress(0.2, desc="📄 Content extracted, generating plan...")
elt_plan = understand_content(content, images, progress)
# Create a downloadable text file
timestamp = time.strftime("%Y%m%d_%H%M%S")
filename = f"ISP_Unit_Plan_{timestamp}.md"
filepath = os.path.join("/tmp", filename) if os.path.exists("/tmp") else filename
with open(filepath, "w", encoding="utf-8") as f:
f.write(elt_plan)
return elt_plan, filepath
except Exception as e:
logging.error(f"Error in generate_elt_plan: {e}")
return f"❌ An error occurred: {str(e)}", None
# Main interface function
def create_main_interface():
with gr.Blocks(theme=gr.themes.Soft(
primary_hue="blue",
secondary_hue="orange",
neutral_hue="gray",
font=["Inter", "system-ui", "sans-serif"]
), css=custom_css) as interface:
# --- MathJax to ensure reliable $begin:math:text$ $end:math:text$ and $begin:math:display$ $end:math:display$ rendering (NEW) ---
gr.HTML("""
<script>
window.MathJax = { tex: { inlineMath: [['\\\$begin:math:text$','\\\\\\$end:math:text$']], displayMath: [['\\\$begin:math:display$','\\\\\\$end:math:display$']] } };
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" async></script>
""")
# Header
gr.HTML("""
<div class="header-banner">
<h1 style="margin: 0; font-size: 2.5em;">🔄 ISP Reverse Unit Planner</h1>
<p style="margin: 10px 0 0 0; opacity: 0.9; font-size: 1.1em;">
Transform your teaching materials into comprehensive unit plans
</p>
</div>
""")
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("""
### 📤 Upload Your Materials
Upload a PDF or PowerPoint presentation containing your teaching materials.
The AI will analyze the content and generate a complete unit plan following ISP standards.
""")
file_input = gr.File(
label="Upload PPTX or PDF",
file_types=[".pdf", ".pptx", ".ppt"],
elem_classes="file-upload"
)
submit_btn = gr.Button(
"🎯 Generate Unit Plan",
variant="primary",
size="lg"
)
with gr.Column(scale=2):
output = gr.Markdown(
label="Generated Unit Plan",
value="*Your unit plan will appear here...*",
elem_classes="output-area"
)
with gr.Row():
copy_btn = gr.Button(
"📋 Copy to Clipboard",
variant="secondary",
size="sm"
)
download_btn = gr.File(
label="📥 Download Unit Plan",
visible=False
)
# Footer
gr.HTML("""
<div class="isp-footer">
<p>🏫 International School of Panama - Reverse Unit Planning Tool v2.5</p>
<p style="font-size: 12px; opacity: 0.7;">
Powered by Gemini
</p>
</div>
""")
# Event handlers
def process_and_show_download(file, progress=gr.Progress()):
output_text, file_path = generate_elt_plan(file, progress)
if file_path:
return output_text, gr.File(value=file_path, visible=True)
else:
return output_text, gr.File(visible=False)
submit_btn.click(
process_and_show_download,
inputs=[file_input],
outputs=[output, download_btn]
)
# Copy to clipboard functionality
copy_btn.click(
fn=None,
inputs=output,
outputs=None,
js="""
async (text) => {
// Extract just the text content, removing any markdown artifacts
const textContent = text;
try {
await navigator.clipboard.writeText(textContent);
// Create success notification
const notification = document.createElement('div');
notification.className = 'success-notification';
notification.style.position = 'fixed';
notification.style.bottom = '20px';
notification.style.left = '50%';
notification.style.transform = 'translateX(-50%)';
notification.style.zIndex = '9999';
notification.innerHTML = '✅ Unit plan copied to clipboard!';
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateX(-50%) translateY(20px)';
setTimeout(() => notification.remove(), 300);
}, 3000);
} catch (err) {
console.error('Failed to copy text: ', err);
alert('Failed to copy to clipboard. Please try selecting and copying manually.');
}
}
"""
)
return interface
# Password-protected login interface
def create_login_interface():
with gr.Blocks(theme=gr.themes.Soft(), css=custom_css) as login_interface:
gr.HTML("""
<div class="login-container">
<div style="text-align: center; margin-bottom: 30px;">
<h1 style="color: #003366; margin-bottom: 10px;">🔐 ISP Reverse Unit Planner</h1>
<p style="color: #666;">Please enter the access password to continue</p>
</div>
</div>
""")
with gr.Column():
password_input = gr.Textbox(
label="Password",
type="password",
placeholder="Enter access password",
elem_classes="password-input"
)
login_btn = gr.Button(
"🔓 Login",
variant="primary",
size="lg"
)
login_status = gr.Markdown("")
# Hidden components for navigation
login_success = gr.State(False)
def handle_login(password):
success, message = verify_password(password)
if success:
return message, True
else:
return message, False
login_btn.click(
handle_login,
inputs=[password_input],
outputs=[login_status, login_success]
)
# Auto-submit on Enter key
password_input.submit(
handle_login,
inputs=[password_input],
outputs=[login_status, login_success]
)
return login_interface, login_success
# Combined interface with authentication
def create_app():
with gr.Blocks(
theme=gr.themes.Soft(
primary_hue="blue",
secondary_hue="indigo",
neutral_hue="slate",
font=["Inter", "system-ui", "sans-serif"],
text_size="md",
spacing_size="md",
radius_size="lg"
),
css=custom_css,
title="ISP Unit Planner"
) as app:
# Inject MathJax globally for reliable math rendering (NEW)
gr.HTML("""
<script>
window.MathJax = { tex: { inlineMath: [['\\\$begin:math:text$','\\\\\\$end:math:text$']], displayMath: [['\\\$begin:math:display$','\\\\\\$end:math:display$']] } };
</script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" async></script>
""")
# State to track authentication
is_authenticated = gr.State(False)
# Login view
with gr.Column(visible=True) as login_view:
gr.HTML("""
<div style="max-width: 450px; margin: 80px auto; padding: 50px; background: linear-gradient(135deg, rgba(255,255,255,0.98) 0%, rgba(240,244,248,0.95) 100%); border-radius: 24px; box-shadow: 0 25px 70px rgba(30, 58, 95, 0.2); border: 1px solid rgba(168, 201, 229, 0.3);">
<div style="text-align: center;">
<h1 style="color: #1e3a5f; margin-bottom: 15px; font-size: 2.2em; font-weight: 300; letter-spacing: -0.5px;">
<span style="font-weight: 700;">ISP</span> Unit Planner
</h1>
<p style="color: #4a7ba7; margin-bottom: 35px; font-size: 16px; font-weight: 400;">Authentication Required</p>
</div>
</div>
""")
with gr.Column():
password_input = gr.Textbox(
label="Password",
type="password",
placeholder="Enter access password",
elem_classes="password-input"
)
login_btn = gr.Button(
"Access Application",
variant="primary",
size="lg"
)
login_status = gr.Markdown("")
# Main application view (initially hidden)
with gr.Column(visible=False) as main_view:
# Header
gr.HTML("""
<div class="header-banner">
<h1 style="margin: 0; font-size: 2.8em; font-weight: 300; letter-spacing: -1px; position: relative; z-index: 1;">
<span style="font-weight: 700;">ISP</span> Reverse Unit Planner
</h1>
<p style="margin: 12px 0 0 0; opacity: 0.95; font-size: 1.15em; font-weight: 300; letter-spacing: 0.5px; position: relative; z-index: 1;">
Transform teaching materials into comprehensive unit plans
</p>
</div>
""")
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("""
### Upload Materials
Select a PDF or PowerPoint file to analyze.
""")
file_input = gr.File(
label="Choose File",
file_types=[".pdf", ".pptx", ".ppt"],
elem_classes="file-upload"
)
submit_btn = gr.Button(
"Generate Unit Plan",
variant="primary",
size="lg"
)
with gr.Column(scale=2):
output = gr.Markdown(
label="Generated Unit Plan",
value="*Your unit plan will appear here...*",
elem_classes="output-area"
)
with gr.Row():
copy_btn = gr.Button(
"Copy to Clipboard",
variant="secondary",
size="sm"
)
download_btn = gr.File(
label="Download",
visible=False
)
# Footer
gr.HTML("""
<div class="isp-footer">
<p style="font-weight: 500; font-size: 15px; margin-bottom: 8px;">International School of Panama</p>
<p style="font-size: 13px; opacity: 0.8;">
ISP Reverse Unit Planner v2.5 • Powered by Gemini
</p>
</div>
""")
# Authentication logic
def authenticate(password):
success, message = verify_password(password)
if success:
return (
gr.update(value=message, visible=True), # login_status
gr.update(visible=False), # login_view
gr.update(visible=True), # main_view
True # is_authenticated
)
else:
return (
gr.update(value=message, visible=True), # login_status
gr.update(visible=True), # login_view
gr.update(visible=False), # main_view
False # is_authenticated
)
# Event handlers for login
login_btn.click(
authenticate,
inputs=[password_input],
outputs=[login_status, login_view, main_view, is_authenticated]
)
password_input.submit(
authenticate,
inputs=[password_input],
outputs=[login_status, login_view, main_view, is_authenticated]
)
# Event handlers for main application
def process_and_show_download(file, progress=gr.Progress()):
output_text, file_path = generate_elt_plan(file, progress)
if file_path:
return output_text, gr.File(value=file_path, visible=True)
else:
return output_text, gr.File(visible=False)
submit_btn.click(
process_and_show_download,
inputs=[file_input],
outputs=[output, download_btn]
)
# Copy to clipboard functionality
copy_btn.click(
fn=None,
inputs=output,
outputs=None,
js="""
async (text) => {
try {
await navigator.clipboard.writeText(text);
// Create success notification
const notification = document.createElement('div');
notification.className = 'success-notification';
notification.style.cssText = `
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: linear-gradient(135deg, #2e86ab 0%, #6fa3d2 100%);
color: white;
padding: 14px 28px;
border-radius: 12px;
z-index: 9999;
font-weight: 500;
box-shadow: 0 8px 24px rgba(46, 134, 171, 0.35);
animation: slideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
`;
notification.innerHTML = '✅ Unit plan copied to clipboard!';
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateX(-50%) translateY(20px)';
setTimeout(() => notification.remove(), 300);
}, 3000);
} catch (err) {
console.error('Failed to copy text: ', err);
alert('Failed to copy to clipboard. Please try selecting and copying manually.');
}
}
"""
)
return app
# Launch the application
if __name__ == "__main__":
# Check if API key is configured
if not GEMINI_API_KEY:
print("⚠️ WARNING: GEMINI_API_KEY environment variable not set!")
print("Please set the GEMINI_API_KEY environment variable before running.")
print("Example: export GEMINI_API_KEY='your-api-key-here'")
if not APP_PASSWORD:
print("⚠️ WARNING: APP_PASSWORD environment variable not set!")
print("Using default password. Please set APP_PASSWORD for production.")
# Create and launch the app
app = create_app()
app.launch(
share=True, # Set to True if you want a public link
server_name="0.0.0.0", # Allow external connections
server_port=7860, # Default Gradio port
favicon_path=None, # You can add a custom favicon here
show_error=True
)