File size: 18,360 Bytes
f57451f
8dfec77
f57451f
 
 
 
8dfec77
8d7ab12
 
 
 
 
2557f60
8d7ab12
f57451f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
import os
import streamlit as st
import requests
import json
from docx import Document
from io import BytesIO

# --- Set Streamlit environment variables for HuggingFace compatibility ---
# This helps prevent permission errors related to Streamlit's internal file operations
# and ensures it doesn't try to write to restricted directories.
os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
os.environ["STREAMLIT_SERVER_ENABLE_ARROW_IPC"] = "false"
os.environ["STREAMLIT_SERVER_FOLDER"] = "/tmp" # Added this line to specify a writable folder

# --- Page Configuration ---
st.set_page_config(
    page_title="Music Lesson Planner",
    page_icon="๐ŸŽถ",
    layout="wide",
    initial_sidebar_state="expanded"
)

# --- Constants and API Setup ---
# IMPORTANT: Set your Google API Key as an environment variable named GOOGLE_API_KEY
# You can get one from Google AI Studio: https://aistudio.google.com/app/apikey
GEMINI_API_KEY = os.getenv('GOOGLE_API_KEY')

# Base URL for Gemini API
GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/"

# Available Gemini Models for comparison
GEMINI_MODELS = {
    "Gemini 2.0 Flash": "gemini-2.0-flash",
    "Gemini 1.5 Pro": "gemini-1.5-pro-latest",
    "Gemini 1.0 Pro": "gemini-1.0-pro",
}


# --- Helper Function for LLM API Call ---
def call_gemini_api(model_name, prompt_text, response_schema=None):
    """
    Calls the Gemini API with the given model and prompt.
    Handles JSON parsing and error reporting.
    """
    if not GEMINI_API_KEY:
        st.error(
            "Gemini API Key is not set. Please set the GOOGLE_API_KEY environment variable or replace `GEMINI_API_KEY = os.getenv('GOOGLE_API_KEY')` with your actual API key.")
        return None

    model_id = GEMINI_MODELS.get(model_name)
    if not model_id:
        st.error(f"Unknown model: {model_name}")
        return None

    url = f"{GEMINI_API_BASE_URL}{model_id}:generateContent?key={GEMINI_API_KEY}"

    headers = {
        "Content-Type": "application/json",
    }

    payload = {
        "contents": [
            {
                "role": "user",
                "parts": [{"text": prompt_text}]
            }
        ],
        "generationConfig": {}  # Initialize generationConfig
    }

    # If a response_schema is provided, configure for structured output
    if response_schema:
        payload["generationConfig"]["responseMimeType"] = "application/json"
        payload["generationConfig"]["responseSchema"] = response_schema
        # Add a clear instruction to the prompt to ensure JSON output
        # This can sometimes help guide the model more effectively.
        prompt_text = f"{prompt_text}\n\nPlease provide the response strictly in JSON format according to the schema provided, with no additional text or markdown outside the JSON object."
        payload["contents"][0]["parts"][0]["text"] = prompt_text

    try:
        response = requests.post(url, headers=headers, json=payload)
        response.raise_for_status()  # Raise an exception for HTTP errors (4xx or 5xx)
        response_data = response.json()

        if response_data and response_data.get("candidates"):
            # Access the text part of the response
            if response_schema:
                # For structured responses, the content is directly the JSON string
                raw_json_text = response_data["candidates"][0]["content"]["parts"][0]["text"]
                try:
                    # Attempt to parse the JSON string
                    parsed_json = json.loads(raw_json_text)
                    return parsed_json
                except json.JSONDecodeError as e:
                    st.error(f"Failed to parse JSON for {model_name} outline. Error: {e}")
                    st.text_area(f"Raw output from {model_name}:", raw_json_text, height=200)
                    return None
            else:
                # For unstructured responses, return the text directly
                return response_data["candidates"][0]["content"]["parts"][0]["text"]
        else:
            st.error(f"No valid response candidates found for {model_name}.")
            st.json(response_data)  # Display the full response for debugging
            return None

    except requests.exceptions.HTTPError as http_err:
        st.error(f"HTTP error occurred for {model_name}: {http_err}")
        st.error(f"Response content: {response.text}")
        return None
    except requests.exceptions.ConnectionError as conn_err:
        st.error(f"Connection error occurred for {model_name}: {conn_err}")
        return None
    except requests.exceptions.Timeout as timeout_err:
        st.error(f"Timeout error occurred for {model_name}: {timeout_err}")
        return None
    except requests.exceptions.RequestException as req_err:
        st.error(f"An unexpected error occurred for {model_name}: {req_err}")
        return None
    except Exception as e:
        st.error(f"An unexpected error occurred during API call for {model_name}: {e}")
        return None


# --- DOCX Conversion Function ---
def create_docx_file(outline_data, draft_text, lesson_topic, lesson_length, model_name):
    """
    Creates a DOCX file from outline data and draft text.
    """
    document = Document()

    # Add Lesson Title
    document.add_heading(outline_data.get("lessonTitle", "Untitled Lesson"), level=1)
    document.add_paragraph(f"Topic: {lesson_topic}")
    document.add_paragraph(f"Length: {lesson_length}")
    document.add_paragraph(f"Generated by: {model_name}")
    document.add_paragraph("\n")  # Add a blank line for spacing

    # Add Learning Objectives
    document.add_heading("Learning Objectives", level=2)
    for obj in outline_data.get("learningObjectives", []):
        document.add_paragraph(obj, style='List Bullet')
    document.add_paragraph("\n")

    # Add Materials
    document.add_heading("Materials", level=2)
    for material in outline_data.get("materials", []):
        document.add_paragraph(material, style='List Bullet')
    document.add_paragraph("\n")

    # Add Procedure
    document.add_heading("Procedure", level=2)
    for section in outline_data.get("procedure", []):
        document.add_heading(f"{section.get('sectionTitle', 'Section')} ({section.get('timeAllocation', 'N/A')})",
                             level=3)
        for activity in section.get("activities", []):
            document.add_paragraph(activity, style='List Bullet')
        document.add_paragraph("\n")

    # Add Assessment
    document.add_heading("Assessment", level=2)
    for assessment_item in outline_data.get("assessment", []):
        document.add_paragraph(assessment_item, style='List Bullet')
    document.add_paragraph("\n")

    # Add Full Draft Content
    document.add_heading("Full Lesson Draft", level=2)
    document.add_paragraph(draft_text)

    # Save document to a BytesIO object
    byte_io = BytesIO()
    document.save(byte_io)
    byte_io.seek(0)  # Rewind the buffer to the beginning
    return byte_io.getvalue()


# --- Password Protection ---
def authenticate_user():
    st.markdown("## ๐Ÿ” Secure Login")
    password = st.text_input("Password", type="password", key="password_input")
    submit = st.button("Login", key="login_button")

    if submit:
        correct_password = os.getenv("APP_PASSWORD")

        if password == correct_password:
            st.session_state["authenticated"] = True
            st.rerun()  # Rerun to clear password input and show app content
        else:
            st.error("Invalid password")


# Check authentication status
if "authenticated" not in st.session_state:
    st.session_state["authenticated"] = False

if not st.session_state["authenticated"]:
    authenticate_user()
    st.stop()  # Stop execution if not authenticated
else:
    # --- Main Application Logic (Protected by Authentication) ---
    st.title("๐ŸŽถ Music Lesson Planner")

    st.markdown("""
    This app helps you draft outlines and detailed lesson plans for online music lessons using different Gemini models.
    Compare outputs to find the best fit for your pedagogical needs!
    """)

    # Initialize session state for outlines and drafts if not already present
    if 'outlines' not in st.session_state:
        st.session_state.outlines = {}
    if 'drafts' not in st.session_state:
        st.session_state.drafts = {}

    # Input fields for lesson details
    with st.sidebar:
        st.header("Lesson Details")
        lesson_topic = st.text_input("Lesson Topic", "Introduction to Solfege")
        lesson_length = st.selectbox("Lesson Length", ["2-minute", "5-minute", "10-minute", "15-minute"], index=1)

        st.header("Model Selection")
        selected_models = st.multiselect(
            "Select Gemini Models",
            list(GEMINI_MODELS.keys()),
            default=["Gemini 2.0 Flash", "Gemini 1.5 Pro"]
        )

        st.header("Prompt Customization")
        default_outline_system_prompt = (
            "You are an AI assistant specialized in creating concise and structured outlines for online music lessons. "
            "Your goal is to provide a clear, pedagogical framework that music educators can easily follow. "
            "Focus on key components: lesson title, learning objectives, materials, a step-by-step procedure with time allocations and activities, and assessment methods."
            "The lessons are online and asynchronous, so ensure the outline is suitable for self-paced learning."
        )
        outline_system_prompt = st.text_area("Outline System Prompt", default_outline_system_prompt, height=150)

        default_outline_user_prompt_template = (
            "Create a {lesson_length} online music lesson outline on the topic of '{lesson_topic}'. "
            "The outline should be structured as a JSON object with the following keys: "
            "'lessonTitle', 'learningObjectives' (list of strings), 'materials' (list of strings), "
            "'procedure' (list of objects, each with 'sectionTitle', 'timeAllocation', and 'activities' (list of strings)), "
            "and 'assessment' (list of strings). "
            "Ensure 'timeAllocation' for each procedure section is a string indicating duration (e.g., '5 minutes'). "
            "Make sure the total time allocations add up to the {lesson_length}."
            "Example for 'procedure' section: "
            '{{"sectionTitle": "Introduction", "timeAllocation": "5 minutes", "activities": ["Greet students", "Review previous concepts"]}}'
        )
        outline_user_prompt_template = st.text_area("Outline User Prompt Template",
                                                    default_outline_user_prompt_template, height=250)

        default_draft_system_prompt = (
            "You are an AI assistant specialized in expanding structured lesson outlines into detailed, engaging rough drafts for online music lessons. "
            "Your goal is to provide specific examples, pedagogical details, and interactive elements for each activity. "
            "The language should be engaging and professional, tailored for music educators."
            "The lessons are online and asynchronous, so ensure the draft is suitable for self-paced learning."
        )
        draft_system_prompt = st.text_area("Draft System Prompt", default_draft_system_prompt, height=150)

        generate_button = st.button("Generate Lesson Plans")

        # Add a logout button to the sidebar
        if st.session_state["authenticated"]:
            if st.button("Logout", key="logout_button_sidebar"):
                st.session_state["authenticated"] = False
                st.rerun()

    # --- Define Outline Schema ---
    outline_response_schema = {
        "type": "OBJECT",
        "properties": {
            "lessonTitle": {"type": "STRING"},
            "learningObjectives": {"type": "ARRAY", "items": {"type": "STRING"}},
            "materials": {"type": "ARRAY", "items": {"type": "STRING"}},
            "procedure": {
                "type": "ARRAY",
                "items": {
                    "type": "OBJECT",
                    "properties": {
                        "sectionTitle": {"type": "STRING"},
                        "timeAllocation": {"type": "STRING"},
                        "activities": {"type": "ARRAY", "items": {"type": "STRING"}}
                    },
                    "required": ["sectionTitle", "timeAllocation", "activities"]
                }
            },
            "assessment": {"type": "ARRAY", "items": {"type": "STRING"}}
        },
        "required": ["lessonTitle", "learningObjectives", "materials", "procedure", "assessment"]
    }

    # --- Lesson Generation Logic (Triggered by button) ---
    if generate_button:
        # Clear previous results when new generation is triggered
        st.session_state.outlines = {}
        st.session_state.drafts = {}
        st.session_state.lesson_topic = lesson_topic  # Store for download
        st.session_state.lesson_length = lesson_length  # Store for download
        st.session_state.selected_models = selected_models  # Store for download

        # Generate outlines
        for model_name in selected_models:
            current_outline_user_prompt = outline_user_prompt_template.format(
                lesson_length=lesson_length,
                lesson_topic=lesson_topic
            )
            full_outline_prompt = f"{outline_system_prompt}\n{current_outline_user_prompt}"
            outline_data = call_gemini_api(model_name, full_outline_prompt, outline_response_schema)
            if outline_data:
                st.session_state.outlines[model_name] = outline_data
            else:
                st.session_state.outlines[model_name] = None  # Mark as failed

        # Generate drafts
        for model_name in selected_models:
            if model_name in st.session_state.outlines and st.session_state.outlines[model_name]:
                outline_for_draft = json.dumps(st.session_state.outlines[model_name], indent=2)
                draft_prompt = (
                    f"{draft_system_prompt}\n\n"
                    f"Expand the following lesson outline into a detailed rough draft for a {lesson_length} lesson. "
                    "Provide specific examples and pedagogical details for each activity. "
                    "Ensure the language is engaging for music educators.\n\n"
                    f"Outline:\n```json\n{outline_for_draft}\n```"
                )
                raw_draft_text = call_gemini_api(model_name, draft_prompt)
                if raw_draft_text:
                    st.session_state.drafts[model_name] = raw_draft_text
                else:
                    st.session_state.drafts[model_name] = None  # Mark as failed
            else:
                st.session_state.drafts[model_name] = None  # Cannot generate draft without outline

    # --- Display Generated Content and Download Buttons (Always displayed if in session_state) ---
    if st.session_state.get('outlines') or st.session_state.get('drafts'):
        st.subheader("Generated Outlines")
        outline_cols = st.columns(len(st.session_state.get('selected_models', [])))
        for i, model_name in enumerate(st.session_state.get('selected_models', [])):
            with outline_cols[i]:
                st.markdown(f"### {model_name} Outline")
                if st.session_state.outlines.get(model_name):
                    st.json(st.session_state.outlines[model_name])
                else:
                    st.markdown(
                        f'<div class="result-box text-gray-500">Could not generate outline for {model_name}.</div>',
                        unsafe_allow_html=True)

        st.subheader("Generated Rough Drafts")
        draft_cols = st.columns(len(st.session_state.get('selected_models', [])))
        for i, model_name in enumerate(st.session_state.get('selected_models', [])):
            with draft_cols[i]:
                st.markdown(f"### {model_name} Rough Draft")
                if st.session_state.drafts.get(model_name):
                    st.markdown(f'<div class="result-box">{st.session_state.drafts[model_name]}</div>',
                                unsafe_allow_html=True)
                else:
                    st.markdown(
                        f'<div class="result-box text-gray-500">Could not generate draft for {model_name}.</div>',
                        unsafe_allow_html=True)

        st.subheader("Download Results")
        download_cols = st.columns(len(st.session_state.get('selected_models', [])))
        for i, model_name in enumerate(st.session_state.get('selected_models', [])):
            with download_cols[i]:
                outline_data = st.session_state.outlines.get(model_name)
                draft_text = st.session_state.drafts.get(model_name)

                if outline_data and draft_text:
                    docx_file = create_docx_file(
                        outline_data,
                        draft_text,
                        st.session_state.lesson_topic,
                        st.session_state.lesson_length,
                        model_name
                    )
                    st.download_button(
                        label=f"Download {model_name} (DOCX)",
                        data=docx_file,
                        file_name=f"{model_name.replace(' ', '_')}_lesson_plan_{st.session_state.lesson_topic.replace(' ', '_')}.docx",
                        mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                        key=f"download_docx_{model_name}"
                    )
                else:
                    st.markdown(
                        f'<div class="text-gray-500">Cannot download DOCX for {model_name} (missing data).</div>',
                        unsafe_allow_html=True)
    elif generate_button:
        st.info("No content generated. Please check for API errors or adjust prompts.")

    st.markdown("""
    <style>
    .result-box {
        border: 1px solid #ddd;
        border-radius: 8px;
        padding: 15px;
        margin-bottom: 10px;
        background-color: #f9f9f9;
        white-space: pre-wrap;
        word-wrap: break-word;
    }
    .text-gray-500 {
        color: #6b7280;
    }
    </style>
    """, unsafe_allow_html=True)