sparshmehta commited on
Commit
671b31d
·
verified ·
1 Parent(s): 7641e4c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +193 -219
app.py CHANGED
@@ -228,41 +228,67 @@ class ContentAnalyzer:
228
  self.GPT4_OUTPUT_COST = 0.60 / 1_000_000 # $0.60 per 1M tokens output
229
 
230
  def analyze_content(self, transcript: str, progress_callback=None) -> Dict[str, Any]:
231
- """Analyze teaching content with retry logic and robust JSON handling"""
232
  for attempt in range(self.retry_count):
233
  try:
234
  if progress_callback:
235
  progress_callback(0.2, "Preparing content analysis...")
236
 
237
  prompt = self._create_analysis_prompt(transcript)
 
 
238
 
239
  if progress_callback:
240
  progress_callback(0.5, "Processing with AI model...")
241
 
242
  try:
243
  response = self.client.chat.completions.create(
244
- model="gpt-4o-mini",
245
  messages=[
246
- {"role": "system", "content": """You are a teaching expert providing a structured analysis.
247
- Analyze the teaching content and provide a detailed assessment in the following format:
248
 
249
- Concept Assessment:
250
- - Subject Matter Accuracy (Score: 0/1, Citations with timestamps)
251
- - First Principles Approach (Score: 0/1, Citations with timestamps)
252
- - Examples and Business Context (Score: 0/1, Citations with timestamps)
253
- - Cohesive Storytelling (Score: 0/1, Citations with timestamps)
254
- - Engagement and Interaction (Score: 0/1, Citations with timestamps)
255
- - Professional Tone (Score: 0/1, Citations with timestamps)
256
 
257
- Code Assessment:
258
- - Depth of Explanation (Score: 0/1, Citations with timestamps)
259
- - Output Interpretation (Score: 0/1, Citations with timestamps)
260
- - Breaking down Complexity (Score: 0/1, Citations with timestamps)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
  Always respond with valid JSON containing these exact categories."""},
263
  {"role": "user", "content": prompt}
264
  ],
265
- response_format={"type": "json_object"}
 
266
  )
267
  logger.info("API call successful")
268
  except Exception as api_error:
@@ -324,7 +350,7 @@ class ContentAnalyzer:
324
  raise
325
 
326
  except Exception as e:
327
- logger.error(f"Content analysis attempt {attempt + 1} failed: {str(e)}")
328
  if attempt == self.retry_count - 1:
329
  logger.error("All attempts failed, returning default structure")
330
  return {
@@ -663,7 +689,7 @@ class MentorEvaluator:
663
  logger.info("Attempting to initialize Whisper model...")
664
  # First try to initialize model with downloading allowed
665
  self._whisper_model = WhisperModel(
666
- "small",
667
  device="cpu",
668
  compute_type="int8",
669
  download_root=self.model_cache_dir,
@@ -676,7 +702,7 @@ class MentorEvaluator:
676
  try:
677
  logger.info("Attempting to load model from local cache...")
678
  self._whisper_model = WhisperModel(
679
- "small",
680
  device="cpu",
681
  compute_type="int8",
682
  download_root=self.model_cache_dir,
@@ -1483,9 +1509,10 @@ def main():
1483
  # Add custom CSS for animations and styling
1484
  st.markdown("""
1485
  <style>
 
1486
  @keyframes fadeIn {
1487
- from { opacity: 0; }
1488
- to { opacity: 1; }
1489
  }
1490
 
1491
  @keyframes slideIn {
@@ -1499,245 +1526,192 @@ def main():
1499
  100% { transform: scale(1); }
1500
  }
1501
 
1502
- .fade-in {
1503
- animation: fadeIn 1s ease-in;
 
 
1504
  }
1505
 
1506
- .slide-in {
1507
- animation: slideIn 0.5s ease-out;
 
1508
  }
1509
 
1510
- .pulse {
1511
- animation: pulse 2s infinite;
 
 
 
 
 
 
 
 
1512
  }
1513
 
1514
- .metric-card {
1515
- background-color: #f0f2f6;
1516
- border-radius: 10px;
1517
- padding: 20px;
1518
- margin: 10px 0;
1519
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
 
1520
  transition: transform 0.3s ease;
1521
  }
1522
 
1523
- .metric-card:hover {
1524
  transform: translateY(-5px);
1525
  }
1526
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1527
  .stButton>button {
 
 
 
 
 
 
1528
  transition: all 0.3s ease;
1529
  }
1530
 
1531
  .stButton>button:hover {
1532
- transform: scale(1.05);
 
1533
  }
1534
 
1535
- .category-header {
1536
- background: linear-gradient(90deg, #1f77b4, #2c3e50);
1537
- color: white;
1538
- padding: 10px;
1539
- border-radius: 5px;
1540
- margin: 10px 0;
1541
  }
1542
 
1543
- .score-badge {
1544
- padding: 5px 10px;
1545
- border-radius: 15px;
1546
- font-weight: bold;
 
 
 
1547
  }
1548
 
1549
- .score-pass {
1550
- background-color: #28a745;
1551
- color: white;
 
 
1552
  }
1553
 
1554
- .score-fail {
1555
- background-color: #dc3545;
1556
- color: white;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1557
  }
1558
  </style>
 
 
 
 
 
 
1559
  """, unsafe_allow_html=True)
1560
 
1561
- # Sidebar with instructions (only once)
1562
  with st.sidebar:
1563
  st.markdown("""
1564
- <div class="slide-in">
1565
- <h2>Instructions</h2>
1566
- <ol>
1567
- <li>Upload your teaching video</li>
1568
- <li>Wait for the analysis</li>
1569
- <li>Review the detailed feedback</li>
 
1570
  <li>Download the report</li>
1571
  </ol>
1572
  </div>
1573
  """, unsafe_allow_html=True)
1574
 
1575
- # Add file format information separately
1576
- st.markdown("**Supported formats:** MP4, AVI, MOV")
1577
- st.markdown("**Maximum file size:** 500MB")
1578
-
1579
- st.header("Processing Status")
1580
- st.info("Upload a video to begin analysis")
1581
-
1582
- # Main title (only once)
1583
- st.markdown("""
1584
- <div class="fade-in">
1585
- <h1 style='text-align: center; color: #1f77b4;'>
1586
- 🎓 Mentor Demo Review System
1587
- </h1>
1588
- </div>
1589
- """, unsafe_allow_html=True)
1590
-
1591
- # Check dependencies with progress
1592
- with st.status("Checking system requirements...") as status:
1593
- progress_bar = st.progress(0)
1594
-
1595
- status.update(label="Checking FFmpeg installation...")
1596
- progress_bar.progress(0.3)
1597
- missing_deps = check_dependencies()
1598
-
1599
- progress_bar.progress(0.6)
1600
- if missing_deps:
1601
- status.update(label="Missing dependencies detected!", state="error")
1602
- st.error(f"Missing required dependencies: {', '.join(missing_deps)}")
1603
- st.markdown("""
1604
- Please install the missing dependencies:
1605
- ```bash
1606
- sudo apt-get update
1607
- sudo apt-get install ffmpeg
1608
- ```
1609
- """)
1610
- return
1611
-
1612
- progress_bar.progress(1.0)
1613
- status.update(label="System requirements satisfied!", state="complete")
1614
-
1615
- # Temporary: Add radio button for input type selection
1616
- input_type = st.radio(
1617
- "Select Input Type (Temporary Feature)",
1618
- ["Video Only", "Video + Transcript"],
1619
- help="Temporary feature: Choose to upload video only or video with transcript"
1620
- )
1621
-
1622
- uploaded_file = st.file_uploader(
1623
- "Upload Teaching Video",
1624
- type=['mp4', 'avi', 'mov'],
1625
- help="Upload your teaching video in MP4, AVI, or MOV format"
1626
- )
1627
-
1628
- # Temporary: Add transcript uploader if Video + Transcript is selected
1629
- uploaded_transcript = None
1630
- if input_type == "Video + Transcript":
1631
- uploaded_transcript = st.file_uploader(
1632
- "Upload Transcript (Optional)",
1633
- type=['txt'],
1634
- help="Upload your transcript in TXT format"
1635
- )
1636
 
1637
  if uploaded_file:
1638
- # Add a pulsing animation while processing
1639
  st.markdown("""
1640
- <div class="pulse" style="text-align: center;">
1641
- <h3>Processing your video...</h3>
 
1642
  </div>
1643
  """, unsafe_allow_html=True)
1644
-
1645
- # Create temp directory for processing
1646
- temp_dir = tempfile.mkdtemp()
1647
- video_path = os.path.join(temp_dir, uploaded_file.name)
1648
-
1649
- try:
1650
- # Save uploaded file with progress
1651
- with st.status("Saving uploaded file...") as status:
1652
- progress_bar = st.progress(0)
1653
-
1654
- # Save in chunks to show progress
1655
- chunk_size = 1024 * 1024 # 1MB chunks
1656
- file_size = len(uploaded_file.getbuffer())
1657
- chunks = file_size // chunk_size + 1
1658
-
1659
- with open(video_path, 'wb') as f:
1660
- for i in range(chunks):
1661
- start = i * chunk_size
1662
- end = min(start + chunk_size, file_size)
1663
- f.write(uploaded_file.getbuffer()[start:end])
1664
- progress = (i + 1) / chunks
1665
- status.update(label=f"Saving file: {progress:.1%}")
1666
- progress_bar.progress(progress)
1667
-
1668
- status.update(label="File saved successfully!", state="complete")
1669
-
1670
- # Validate file size
1671
- file_size = os.path.getsize(video_path) / (1024 * 1024 * 1024) # Size in GB
1672
- if file_size > 2:
1673
- st.error("File size exceeds 2GB limit. Please upload a smaller file.")
1674
- return
1675
-
1676
- # Store evaluation results in session state
1677
- if 'evaluation_results' not in st.session_state:
1678
- # Process video only if results aren't already in session state
1679
- with st.spinner("Processing video"):
1680
- evaluator = MentorEvaluator()
1681
-
1682
- # Temporary: Handle transcript if provided
1683
- if uploaded_transcript:
1684
- transcript_text = uploaded_transcript.getvalue().decode('utf-8')
1685
- # Extract audio features but skip transcription
1686
- audio_features = evaluator.feature_extractor.extract_features(video_path)
1687
-
1688
- # Evaluate speech metrics
1689
- speech_metrics = evaluator._evaluate_speech_metrics(
1690
- transcript_text,
1691
- audio_features
1692
- )
1693
-
1694
- # Analyze content
1695
- content_analysis = evaluator.content_analyzer.analyze_content(transcript_text)
1696
-
1697
- # Generate recommendations
1698
- recommendations = evaluator.recommendation_generator.generate_recommendations(
1699
- speech_metrics,
1700
- content_analysis
1701
- )
1702
-
1703
- # Combine results
1704
- st.session_state.evaluation_results = {
1705
- "communication": speech_metrics,
1706
- "teaching": content_analysis,
1707
- "recommendations": recommendations,
1708
- "transcript": transcript_text
1709
- }
1710
- else:
1711
- # Original flow: full video evaluation
1712
- st.session_state.evaluation_results = evaluator.evaluate_video(video_path)
1713
-
1714
- # Display results using stored evaluation
1715
- st.success("Analysis complete!")
1716
- display_evaluation(st.session_state.evaluation_results)
1717
-
1718
- # Add download button using stored results
1719
- if st.download_button(
1720
- "📥 Download Full Report",
1721
- json.dumps(st.session_state.evaluation_results, indent=2),
1722
- "evaluation_report.json",
1723
- "application/json",
1724
- help="Download the complete evaluation report in JSON format"
1725
- ):
1726
- st.success("Report downloaded successfully!")
1727
-
1728
- # Debugging code
1729
- # if st.session_state.evaluation_results:
1730
- # st.write("Debug: Teaching Analysis Structure")
1731
- # teaching_data = st.session_state.evaluation_results.get("teaching", {})
1732
- # st.json(teaching_data)
1733
-
1734
- except Exception as e:
1735
- st.error(f"Error during evaluation: {str(e)}")
1736
-
1737
- finally:
1738
- # Clean up temp files
1739
- if 'temp_dir' in locals():
1740
- shutil.rmtree(temp_dir)
1741
 
1742
  except Exception as e:
1743
  st.error(f"Application error: {str(e)}")
 
228
  self.GPT4_OUTPUT_COST = 0.60 / 1_000_000 # $0.60 per 1M tokens output
229
 
230
  def analyze_content(self, transcript: str, progress_callback=None) -> Dict[str, Any]:
231
+ """Analyze teaching content with more lenient validation and robust JSON handling"""
232
  for attempt in range(self.retry_count):
233
  try:
234
  if progress_callback:
235
  progress_callback(0.2, "Preparing content analysis...")
236
 
237
  prompt = self._create_analysis_prompt(transcript)
238
+ logger.info(f"Attempt {attempt + 1}: Sending analysis request")
239
+ logger.info(f"Transcript length: {len(transcript)} characters")
240
 
241
  if progress_callback:
242
  progress_callback(0.5, "Processing with AI model...")
243
 
244
  try:
245
  response = self.client.chat.completions.create(
246
+ model="gpt-4o-mini", # Keeping original model
247
  messages=[
248
+ {"role": "system", "content": """You are a strict teaching evaluator focusing on core teaching competencies.
249
+ Ignore minor transcription errors or verbal stumbles. Focus on substantive teaching quality.
250
 
251
+ Score of 1 should be given when CORE criteria below are met with clear evidence.
252
+ Default to 0 if MAJOR teaching deficiencies are present.
 
 
 
 
 
253
 
254
+ Concept Assessment Scoring Criteria:
255
+ - Subject Matter Accuracy (Score 1 requires: Core concepts are accurately explained. Ignore minor verbal slips)
256
+ - First Principles Approach (Score 1 requires: Clear explanation of fundamentals before advanced concepts)
257
+ - Examples and Business Context (Score 1 requires: Multiple relevant examples with clear business impact)
258
+ - Cohesive Storytelling (Score 1 requires: Clear logical progression of concepts. Minor verbal transitions can be ignored)
259
+ - Engagement and Interaction (Score 1 requires: Regular attempts to engage learners with meaningful questions)
260
+ - Professional Tone (Score 1 requires: Overall professional delivery. Ignore occasional filler words)
261
+
262
+ Code Assessment Scoring Criteria:
263
+ - Depth of Explanation (Score 1 requires: Clear explanation of key implementation aspects)
264
+ - Output Interpretation (Score 1 requires: Clear connection between code and business outcomes)
265
+ - Breaking down Complexity (Score 1 requires: Systematic breakdown of complex concepts)
266
+
267
+ What to Ignore:
268
+ - Minor transcription errors
269
+ - Occasional verbal stumbles or filler words
270
+ - Small grammatical mistakes
271
+ - Brief pauses or hesitations
272
+ - Minor code syntax errors in verbal explanation
273
+
274
+ What to Focus On:
275
+ - Accuracy of core technical concepts
276
+ - Clarity of fundamental explanations
277
+ - Quality of real-world examples
278
+ - Overall teaching structure and flow
279
+ - Meaningful learner engagement
280
+ - Key implementation explanations
281
+
282
+ Citations Requirements:
283
+ - Include timestamps [MM:SS] where possible
284
+ - Focus on substantive teaching moments
285
+ - Provide specific examples of effective or ineffective teaching
286
 
287
  Always respond with valid JSON containing these exact categories."""},
288
  {"role": "user", "content": prompt}
289
  ],
290
+ response_format={"type": "json_object"},
291
+ temperature=0.4 # Slightly higher temperature for more lenient evaluation
292
  )
293
  logger.info("API call successful")
294
  except Exception as api_error:
 
350
  raise
351
 
352
  except Exception as e:
353
+ logger.error(f"Content analysis attempt {attempt + 1} failed: {e}")
354
  if attempt == self.retry_count - 1:
355
  logger.error("All attempts failed, returning default structure")
356
  return {
 
689
  logger.info("Attempting to initialize Whisper model...")
690
  # First try to initialize model with downloading allowed
691
  self._whisper_model = WhisperModel(
692
+ "medium",
693
  device="cpu",
694
  compute_type="int8",
695
  download_root=self.model_cache_dir,
 
702
  try:
703
  logger.info("Attempting to load model from local cache...")
704
  self._whisper_model = WhisperModel(
705
+ "medium",
706
  device="cpu",
707
  compute_type="int8",
708
  download_root=self.model_cache_dir,
 
1509
  # Add custom CSS for animations and styling
1510
  st.markdown("""
1511
  <style>
1512
+ /* Modern animations */
1513
  @keyframes fadeIn {
1514
+ from { opacity: 0; transform: translateY(20px); }
1515
+ to { opacity: 1; transform: translateY(0); }
1516
  }
1517
 
1518
  @keyframes slideIn {
 
1526
  100% { transform: scale(1); }
1527
  }
1528
 
1529
+ @keyframes gradientBG {
1530
+ 0% { background-position: 0% 50%; }
1531
+ 50% { background-position: 100% 50%; }
1532
+ 100% { background-position: 0% 50%; }
1533
  }
1534
 
1535
+ /* Modern styling */
1536
+ .stApp {
1537
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
1538
  }
1539
 
1540
+ .main-title {
1541
+ text-align: center;
1542
+ color: #2c3e50;
1543
+ font-size: 2.5rem;
1544
+ font-weight: 700;
1545
+ margin: 2rem 0;
1546
+ padding: 1rem;
1547
+ border-radius: 10px;
1548
+ background: linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%);
1549
+ animation: fadeIn 1s ease-out;
1550
  }
1551
 
1552
+ .card {
1553
+ background: white;
1554
+ padding: 1.5rem;
1555
+ border-radius: 15px;
1556
+ box-shadow: 0 10px 20px rgba(0,0,0,0.1);
1557
+ margin: 1rem 0;
1558
+ animation: fadeIn 0.5s ease-out;
1559
  transition: transform 0.3s ease;
1560
  }
1561
 
1562
+ .card:hover {
1563
  transform: translateY(-5px);
1564
  }
1565
 
1566
+ .metric-card {
1567
+ background: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%);
1568
+ color: #1a202c;
1569
+ padding: 1rem;
1570
+ border-radius: 10px;
1571
+ text-align: center;
1572
+ animation: fadeIn 0.5s ease-out;
1573
+ }
1574
+
1575
+ .sidebar-content {
1576
+ background: rgba(255, 255, 255, 0.9);
1577
+ padding: 1.5rem;
1578
+ border-radius: 10px;
1579
+ margin: 1rem 0;
1580
+ }
1581
+
1582
  .stButton>button {
1583
+ background: linear-gradient(120deg, #4facfe 0%, #00f2fe 100%);
1584
+ color: white;
1585
+ border: none;
1586
+ padding: 0.75rem 1.5rem;
1587
+ border-radius: 25px;
1588
+ font-weight: 600;
1589
  transition: all 0.3s ease;
1590
  }
1591
 
1592
  .stButton>button:hover {
1593
+ transform: translateY(-2px);
1594
+ box-shadow: 0 5px 15px rgba(0,0,0,0.2);
1595
  }
1596
 
1597
+ .stProgress > div > div {
1598
+ background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
 
 
 
 
1599
  }
1600
 
1601
+ /* Status indicators */
1602
+ .status-processing {
1603
+ background: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%);
1604
+ padding: 1rem;
1605
+ border-radius: 10px;
1606
+ text-align: center;
1607
+ animation: pulse 2s infinite;
1608
  }
1609
 
1610
+ .status-complete {
1611
+ background: linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%);
1612
+ padding: 1rem;
1613
+ border-radius: 10px;
1614
+ text-align: center;
1615
  }
1616
 
1617
+ /* Expander styling */
1618
+ .streamlit-expanderHeader {
1619
+ background: linear-gradient(90deg, #f6f9fc 0%, #f0f4f8 100%);
1620
+ border-radius: 8px;
1621
+ padding: 0.5rem 1rem;
1622
+ font-weight: 600;
1623
+ }
1624
+
1625
+ /* File uploader styling */
1626
+ .uploadedFile {
1627
+ background: white;
1628
+ padding: 1rem;
1629
+ border-radius: 10px;
1630
+ box-shadow: 0 5px 15px rgba(0,0,0,0.1);
1631
+ }
1632
+
1633
+ /* Metrics styling */
1634
+ div[data-testid="stMetricValue"] {
1635
+ font-size: 2rem;
1636
+ font-weight: 700;
1637
+ color: #2c3e50;
1638
+ }
1639
+
1640
+ /* Success/Error message styling */
1641
+ .stSuccess, .stError {
1642
+ padding: 1rem;
1643
+ border-radius: 10px;
1644
+ animation: fadeIn 0.5s ease-out;
1645
  }
1646
  </style>
1647
+
1648
+ <div class="fade-in">
1649
+ <h1 class="main-title">
1650
+ 🎓 Mentor Demo Review System
1651
+ </h1>
1652
+ </div>
1653
  """, unsafe_allow_html=True)
1654
 
1655
+ # Sidebar with modern styling
1656
  with st.sidebar:
1657
  st.markdown("""
1658
+ <div class="sidebar-content">
1659
+ <h2 style='text-align: center; color: #2c3e50;'>📋 Instructions</h2>
1660
+ <p style='color: #4a5568;'>Follow these steps to evaluate your teaching demo:</p>
1661
+ <ol style='color: #4a5568;'>
1662
+ <li>Upload your teaching demo video</li>
1663
+ <li>Wait for the analysis to complete</li>
1664
+ <li>Review your detailed evaluation</li>
1665
  <li>Download the report</li>
1666
  </ol>
1667
  </div>
1668
  """, unsafe_allow_html=True)
1669
 
1670
+ st.markdown("""
1671
+ <div class="sidebar-content">
1672
+ <h3 style='color: #2c3e50;'>📁 File Requirements</h3>
1673
+ <p style='color: #4a5568;'><strong>Supported formats:</strong> MP4, AVI, MOV<br>
1674
+ <strong>Maximum file size:</strong> 500MB</p>
1675
+ </div>
1676
+ """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1677
 
1678
  if uploaded_file:
1679
+ # Add a modern processing animation
1680
  st.markdown("""
1681
+ <div class="status-processing">
1682
+ <h3>🔄 Processing Your Video</h3>
1683
+ <p>Please wait while we analyze your teaching demo...</p>
1684
  </div>
1685
  """, unsafe_allow_html=True)
1686
+
1687
+ # Display results using stored evaluation
1688
+ st.markdown("""
1689
+ <div class="status-complete">
1690
+ <h3>✅ Analysis Complete!</h3>
1691
+ <p>Review your detailed evaluation below</p>
1692
+ </div>
1693
+ """, unsafe_allow_html=True)
1694
+
1695
+ # Wrap the evaluation display in a card
1696
+ st.markdown('<div class="card">', unsafe_allow_html=True)
1697
+ display_evaluation(st.session_state.evaluation_results)
1698
+ st.markdown('</div>', unsafe_allow_html=True)
1699
+
1700
+ # Modern download button
1701
+ st.markdown("""
1702
+ <div class="card" style="text-align: center;">
1703
+ <h3 style='color: #2c3e50;'>📥 Download Your Report</h3>
1704
+ </div>
1705
+ """, unsafe_allow_html=True)
1706
+
1707
+ if st.download_button(
1708
+ "Download Full Report",
1709
+ json.dumps(st.session_state.evaluation_results, indent=2),
1710
+ "evaluation_report.json",
1711
+ "application/json",
1712
+ help="Download the complete evaluation report in JSON format"
1713
+ ):
1714
+ st.success("Report downloaded successfully!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1715
 
1716
  except Exception as e:
1717
  st.error(f"Application error: {str(e)}")