sparshmehta commited on
Commit
e1d9244
·
verified ·
1 Parent(s): e36dab5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +573 -243
app.py CHANGED
@@ -48,22 +48,44 @@ def temporary_file(suffix: Optional[str] = None):
48
  logger.warning(f"Failed to remove temporary file {temp_path}: {e}")
49
 
50
  class ProgressTracker:
51
- """Handles progress tracking and ETA calculations"""
52
  def __init__(self, status_element, progress_bar):
53
  self.status = status_element
54
  self.progress = progress_bar
55
  self.start_time = time.time()
 
 
 
 
 
 
 
 
 
56
 
57
- def update(self, progress: float, message: str):
58
- """Update progress with ETA calculation"""
59
- self.status.update(label=f"{message} ({progress:.1%})")
60
- self.progress.progress(progress)
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  if progress > 0:
63
  elapsed = time.time() - self.start_time
64
- estimated_total = elapsed / progress
65
- remaining = estimated_total - elapsed
66
- self.status.update(label=f"{message} ({progress:.1%}) - ETA: {remaining:.0f}s")
 
 
67
 
68
  class AudioFeatureExtractor:
69
  """Handles audio feature extraction with improved pause detection"""
@@ -224,8 +246,6 @@ class ContentAnalyzer:
224
  self.client = OpenAI(api_key=api_key)
225
  self.retry_count = 3
226
  self.retry_delay = 1
227
- self.GPT4_INPUT_COST = 0.15 / 1_000_000 # $0.15 per 1M tokens input
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"""
@@ -350,7 +370,7 @@ class ContentAnalyzer:
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 {
@@ -657,6 +677,55 @@ Consider:
657
  - Use of examples and analogies
658
  - Engagement style"""
659
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
660
  class MentorEvaluator:
661
  """Main class for video evaluation"""
662
  def __init__(self, model_cache_dir: Optional[str] = None):
@@ -677,9 +746,7 @@ class MentorEvaluator:
677
  self._feature_extractor = None
678
  self._content_analyzer = None
679
  self._recommendation_generator = None
680
-
681
- # Cost per minute for Whisper transcription
682
- self.WHISPER_COST_PER_MINUTE = 0.006 # $0.006 per minute of audio
683
 
684
  @property
685
  def whisper_model(self):
@@ -689,7 +756,7 @@ class MentorEvaluator:
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,7 +769,7 @@ class MentorEvaluator:
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,
@@ -739,7 +806,7 @@ class MentorEvaluator:
739
  return self._recommendation_generator
740
 
741
  def evaluate_video(self, video_path: str) -> Dict[str, Any]:
742
- """Evaluate video with proper resource management"""
743
  with temporary_file(suffix=".wav") as temp_audio:
744
  try:
745
  # Extract audio
@@ -748,49 +815,52 @@ class MentorEvaluator:
748
  tracker = ProgressTracker(status, progress_bar)
749
  self._extract_audio(video_path, temp_audio, tracker.update)
750
 
751
- # Extract features
752
- with st.status("Extracting audio features...") as status:
753
- progress_bar = st.progress(0)
754
- tracker = ProgressTracker(status, progress_bar)
755
- audio_features = self.feature_extractor.extract_features(
756
- temp_audio,
757
- tracker.update
758
- )
759
 
760
- # Transcribe
761
- with st.status("Transcribing audio...") as status:
762
- progress_bar = st.progress(0)
763
- tracker = ProgressTracker(status, progress_bar)
764
- transcript = self._transcribe_audio(temp_audio, tracker.update)
 
 
 
 
 
 
 
 
 
 
765
 
766
- # Analyze content
767
- with st.status("Analyzing content...") as status:
768
- progress_bar = st.progress(0)
769
- tracker = ProgressTracker(status, progress_bar)
770
- content_analysis = self.content_analyzer.analyze_content(
771
- transcript,
772
- tracker.update
773
- )
774
 
775
- # Evaluate speech
776
- with st.status("Evaluating speech metrics...") as status:
777
- progress_bar = st.progress(0)
778
- tracker = ProgressTracker(status, progress_bar)
779
- speech_metrics = self._evaluate_speech_metrics(
780
- transcript,
781
- audio_features,
782
- tracker.update
783
- )
 
 
 
 
 
 
784
 
785
- # Generate recommendations
786
- with st.status("Generating recommendations...") as status:
787
- progress_bar = st.progress(0)
788
- tracker = ProgressTracker(status, progress_bar)
789
- recommendations = self.recommendation_generator.generate_recommendations(
790
- speech_metrics,
791
- content_analysis,
792
- tracker.update
793
- )
794
 
795
  return {
796
  "communication": speech_metrics,
@@ -854,47 +924,134 @@ class MentorEvaluator:
854
  raise AudioProcessingError(f"Audio extraction failed: {str(e)}")
855
 
856
  def _transcribe_audio(self, audio_path: str, progress_callback=None) -> str:
857
- """Transcribe audio with improved memory management"""
858
  try:
859
  if progress_callback:
860
  progress_callback(0.1, "Loading transcription model...")
861
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
862
  audio_info = sf.info(audio_path)
863
  total_duration = audio_info.duration
864
- chunk_duration = 5 * 60 # 5-minute chunks
865
- overlap_duration = 10 # 10-second overlap
866
-
867
- transcripts = []
868
- total_chunks = int(np.ceil(total_duration / (chunk_duration - overlap_duration)))
869
-
870
- with sf.SoundFile(audio_path) as f:
871
- for i in range(total_chunks):
872
- if progress_callback:
873
- progress_callback(0.4 + (i / total_chunks) * 0.4,
874
- f"Transcribing chunk {i + 1}/{total_chunks}...")
875
-
876
- # Calculate positions in samples
877
- start_sample = int(i * (chunk_duration - overlap_duration) * f.samplerate)
878
- f.seek(start_sample)
879
- chunk = f.read(frames=int(chunk_duration * f.samplerate))
880
-
881
- with temporary_file(suffix=".wav") as chunk_path:
882
- sf.write(chunk_path, chunk, f.samplerate)
883
- # The fix: properly handle the segments from faster-whisper
884
- segments, _ = self.whisper_model.transcribe(chunk_path)
885
- # Combine all segment texts
886
- chunk_text = ' '.join(segment.text for segment in segments)
887
- transcripts.append(chunk_text)
888
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
889
  if progress_callback:
890
  progress_callback(1.0, "Transcription complete!")
891
-
892
- return " ".join(transcripts)
893
-
894
  except Exception as e:
895
  logger.error(f"Error in transcription: {e}")
896
  raise
897
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
898
  def calculate_speech_metrics(self, transcript: str, audio_duration: float) -> Dict[str, float]:
899
  """Calculate words per minute and other speech metrics."""
900
  words = len(transcript.split())
@@ -1215,12 +1372,68 @@ def display_evaluation(evaluation: Dict[str, Any]):
1215
 
1216
  recommendations = evaluation.get("recommendations", {})
1217
 
1218
- # Geography Fit with improved formatting
1219
- # with st.expander("🌍 Geography Fit", expanded=True):
1220
- # geography_fit = recommendations.get("geographyFit", "Not specified")
1221
- # st.info(geography_fit)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1222
 
1223
- # Improvements Needed with better formatting
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1224
  with st.expander("💡 Areas for Improvement", expanded=True):
1225
  improvements = recommendations.get("improvements", [])
1226
  if isinstance(improvements, list):
@@ -1501,6 +1714,108 @@ def check_dependencies() -> List[str]:
1501
 
1502
  return missing
1503
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1504
  def main():
1505
  try:
1506
  # Set page config must be the first Streamlit command
@@ -1509,10 +1824,35 @@ def main():
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,153 +1866,92 @@ def main():
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
  }
1560
 
1561
- .card:hover {
1562
- transform: translateY(-5px);
1563
  }
1564
 
1565
  .metric-card {
1566
- background: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%);
1567
- color: #1a202c;
1568
- padding: 1rem;
1569
  border-radius: 10px;
1570
- text-align: center;
1571
- animation: fadeIn 0.5s ease-out;
 
 
1572
  }
1573
 
1574
- .sidebar-content {
1575
- background: rgba(255, 255, 255, 0.9);
1576
- padding: 1.5rem;
1577
- border-radius: 10px;
1578
- margin: 1rem 0;
1579
  }
1580
 
1581
  .stButton>button {
1582
- background: linear-gradient(120deg, #4facfe 0%, #00f2fe 100%);
1583
- color: white;
1584
- border: none;
1585
- padding: 0.75rem 1.5rem;
1586
- border-radius: 25px;
1587
- font-weight: 600;
1588
  transition: all 0.3s ease;
1589
  }
1590
 
1591
  .stButton>button:hover {
1592
- transform: translateY(-2px);
1593
- box-shadow: 0 5px 15px rgba(0,0,0,0.2);
1594
  }
1595
 
1596
- .stProgress > div > div {
1597
- background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
1598
- }
1599
-
1600
- /* Status indicators */
1601
- .status-processing {
1602
- background: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%);
1603
- padding: 1rem;
1604
- border-radius: 10px;
1605
- text-align: center;
1606
- animation: pulse 2s infinite;
1607
- }
1608
-
1609
- .status-complete {
1610
- background: linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%);
1611
- padding: 1rem;
1612
- border-radius: 10px;
1613
- text-align: center;
1614
- }
1615
-
1616
- /* Expander styling */
1617
- .streamlit-expanderHeader {
1618
- background: linear-gradient(90deg, #f6f9fc 0%, #f0f4f8 100%);
1619
- border-radius: 8px;
1620
- padding: 0.5rem 1rem;
1621
- font-weight: 600;
1622
  }
1623
 
1624
- /* File uploader styling */
1625
- .uploadedFile {
1626
- background: white;
1627
- padding: 1rem;
1628
- border-radius: 10px;
1629
- box-shadow: 0 5px 15px rgba(0,0,0,0.1);
1630
  }
1631
 
1632
- /* Metrics styling */
1633
- div[data-testid="stMetricValue"] {
1634
- font-size: 2rem;
1635
- font-weight: 700;
1636
- color: #2c3e50;
1637
  }
1638
 
1639
- /* Success/Error message styling */
1640
- .stSuccess, .stError {
1641
- padding: 1rem;
1642
- border-radius: 10px;
1643
- animation: fadeIn 0.5s ease-out;
1644
  }
1645
  </style>
1646
 
1647
  <div class="fade-in">
1648
- <h1 class="main-title">
1649
  🎓 Mentor Demo Review System
1650
  </h1>
1651
  </div>
1652
  """, unsafe_allow_html=True)
1653
 
1654
- # Sidebar with modern styling
1655
  with st.sidebar:
1656
  st.markdown("""
1657
- <div class="sidebar-content">
1658
- <h2 style='text-align: center; color: #2c3e50;'>📋 Instructions</h2>
1659
- <p style='color: #4a5568;'>Follow these steps to evaluate your teaching demo:</p>
1660
- <ol style='color: #4a5568;'>
1661
- <li>Upload your teaching demo video</li>
1662
- <li>Wait for the analysis to complete</li>
1663
- <li>Review your detailed evaluation</li>
1664
  <li>Download the report</li>
1665
  </ol>
1666
  </div>
1667
  """, unsafe_allow_html=True)
1668
 
1669
- st.markdown("""
1670
- <div class="sidebar-content">
1671
- <h3 style='color: #2c3e50;'>📁 File Requirements</h3>
1672
- <p style='color: #4a5568;'><strong>Supported formats:</strong> MP4, AVI, MOV<br>
1673
- <strong>Maximum file size:</strong> 500MB</p>
1674
- </div>
1675
- """, unsafe_allow_html=True)
1676
 
1677
  # Check dependencies with progress
1678
  with st.status("Checking system requirements...") as status:
@@ -1698,25 +1977,36 @@ def main():
1698
  progress_bar.progress(1.0)
1699
  status.update(label="System requirements satisfied!", state="complete")
1700
 
1701
- # File uploader with modern styling
1702
- st.markdown("""
1703
- <div class="card">
1704
- <h3 style='color: #2c3e50; text-align: center;'>📤 Upload Your Teaching Video</h3>
1705
- </div>
1706
- """, unsafe_allow_html=True)
1707
-
1708
  uploaded_file = st.file_uploader(
1709
- "Choose a video file",
1710
  type=['mp4', 'avi', 'mov'],
1711
  help="Upload your teaching video in MP4, AVI, or MOV format"
1712
  )
 
 
 
 
 
 
 
 
 
1713
 
1714
  if uploaded_file:
1715
- # Add a modern processing animation
 
 
 
1716
  st.markdown("""
1717
- <div class="status-processing">
1718
- <h3>🔄 Processing Your Video</h3>
1719
- <p>Please wait while we analyze your teaching demo...</p>
1720
  </div>
1721
  """, unsafe_allow_html=True)
1722
 
@@ -1727,6 +2017,8 @@ def main():
1727
  try:
1728
  # Save uploaded file with progress
1729
  with st.status("Saving uploaded file...") as status:
 
 
1730
  progress_bar = st.progress(0)
1731
 
1732
  # Save in chunks to show progress
@@ -1746,49 +2038,87 @@ def main():
1746
  status.update(label="File saved successfully!", state="complete")
1747
 
1748
  # Validate file size
1749
- file_size = os.path.getsize(video_path) / (1024 * 1024) # Size in MB
1750
- if file_size > 500: # 500MB limit
1751
- st.error("File size exceeds 500MB limit. Please upload a smaller file.")
1752
  return
1753
 
1754
- # Process video and store results
1755
  if 'evaluation_results' not in st.session_state:
 
 
 
 
1756
  with st.spinner("Processing video"):
1757
  evaluator = MentorEvaluator()
1758
- st.session_state.evaluation_results = evaluator.evaluate_video(video_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1759
 
1760
- # Display completion status
1761
- st.markdown("""
1762
- <div class="status-complete">
1763
- <h3>✅ Analysis Complete!</h3>
1764
- <p>Review your detailed evaluation below</p>
1765
- </div>
1766
- """, unsafe_allow_html=True)
1767
 
1768
- # Display evaluation in a card
1769
- st.markdown('<div class="card">', unsafe_allow_html=True)
1770
  display_evaluation(st.session_state.evaluation_results)
1771
- st.markdown('</div>', unsafe_allow_html=True)
1772
 
1773
- # Download section
1774
- st.markdown("""
1775
- <div class="card" style="text-align: center;">
1776
- <h3 style='color: #2c3e50;'>📥 Download Your Report</h3>
1777
- </div>
1778
- """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1779
 
1780
- if st.download_button(
1781
- "Download Full Report",
1782
- json.dumps(st.session_state.evaluation_results, indent=2),
1783
- "evaluation_report.json",
1784
- "application/json",
1785
- help="Download the complete evaluation report in JSON format"
1786
- ):
1787
- st.success("Report downloaded successfully!")
1788
-
1789
  except Exception as e:
 
 
1790
  st.error(f"Error during evaluation: {str(e)}")
1791
-
1792
  finally:
1793
  # Clean up temp files
1794
  if 'temp_dir' in locals():
 
48
  logger.warning(f"Failed to remove temporary file {temp_path}: {e}")
49
 
50
  class ProgressTracker:
51
+ """Handles progress tracking and ETA calculations with step tracking"""
52
  def __init__(self, status_element, progress_bar):
53
  self.status = status_element
54
  self.progress = progress_bar
55
  self.start_time = time.time()
56
+ self.current_step = ""
57
+ self.total_steps = [
58
+ "Loading Audio",
59
+ "Extracting Features",
60
+ "Transcribing Audio",
61
+ "Analyzing Content",
62
+ "Generating Recommendations"
63
+ ]
64
+ self.step_index = 0
65
 
66
+ def update(self, progress: float, message: str, batch_info: str = None):
67
+ """Update progress with ETA calculation and step tracking"""
68
+ # Update current step if it's changed
69
+ if message != self.current_step:
70
+ self.current_step = message
71
+ self.step_index = self.total_steps.index(message) if message in self.total_steps else self.step_index
72
+
73
+ # Calculate overall progress including step progress
74
+ overall_progress = (self.step_index + progress) / len(self.total_steps)
75
+ self.progress.progress(overall_progress)
76
+
77
+ # Format status message
78
+ status_msg = f"Step {self.step_index + 1}/{len(self.total_steps)}: {message}"
79
+ if batch_info:
80
+ status_msg += f" | {batch_info}"
81
 
82
  if progress > 0:
83
  elapsed = time.time() - self.start_time
84
+ estimated_total = elapsed / overall_progress if overall_progress > 0 else 0
85
+ remaining = max(0, estimated_total - elapsed)
86
+ status_msg += f" ({progress:.1%}) - ETA: {remaining:.0f}s"
87
+
88
+ self.status.update(label=status_msg)
89
 
90
  class AudioFeatureExtractor:
91
  """Handles audio feature extraction with improved pause detection"""
 
246
  self.client = OpenAI(api_key=api_key)
247
  self.retry_count = 3
248
  self.retry_delay = 1
 
 
249
 
250
  def analyze_content(self, transcript: str, progress_callback=None) -> Dict[str, Any]:
251
  """Analyze teaching content with more lenient validation and robust JSON handling"""
 
370
  raise
371
 
372
  except Exception as e:
373
+ logger.error(f"Content analysis attempt {attempt + 1} failed: {str(e)}")
374
  if attempt == self.retry_count - 1:
375
  logger.error("All attempts failed, returning default structure")
376
  return {
 
677
  - Use of examples and analogies
678
  - Engagement style"""
679
 
680
+ class CostCalculator:
681
+ """Calculates API and processing costs"""
682
+ def __init__(self):
683
+ self.GPT4_INPUT_COST = 0.15 / 1_000_000 # $0.15 per 1M tokens input
684
+ self.GPT4_OUTPUT_COST = 0.60 / 1_000_000 # $0.60 per 1M tokens output
685
+ self.costs = {
686
+ 'transcription': 0.0,
687
+ 'content_analysis': 0.0,
688
+ 'recommendations': 0.0,
689
+ 'total': 0.0
690
+ }
691
+
692
+ def estimate_tokens(self, text: str) -> int:
693
+ """Rough estimation of token count based on words"""
694
+ return len(text.split()) * 1.3 # Approximate tokens per word
695
+
696
+ def add_transcription_cost(self, duration_seconds: float):
697
+ """Calculate Whisper transcription cost"""
698
+ # Assuming a fixed rate per minute of audio
699
+ cost = (duration_seconds / 60) * 0.006 # $0.006 per minute
700
+ self.costs['transcription'] = cost
701
+ self.costs['total'] += cost
702
+ print(f"\nTranscription Cost: ${cost:.4f}")
703
+
704
+ def add_gpt4_cost(self, input_text: str, output_text: str, operation: str):
705
+ """Calculate GPT-4 API cost for a single operation"""
706
+ input_tokens = self.estimate_tokens(input_text)
707
+ output_tokens = self.estimate_tokens(output_text)
708
+
709
+ input_cost = input_tokens * self.GPT4_INPUT_COST
710
+ output_cost = output_tokens * self.GPT4_OUTPUT_COST
711
+ total_cost = input_cost + output_cost
712
+
713
+ self.costs[operation] = total_cost
714
+ self.costs['total'] += total_cost
715
+
716
+ print(f"\n{operation.replace('_', ' ').title()} Cost:")
717
+ print(f"Input tokens: {input_tokens:.0f} (${input_cost:.4f})")
718
+ print(f"Output tokens: {output_tokens:.0f} (${output_cost:.4f})")
719
+ print(f"Operation total: ${total_cost:.4f}")
720
+
721
+ def print_total_cost(self):
722
+ """Print total cost breakdown"""
723
+ print("\n=== Cost Breakdown ===")
724
+ for key, cost in self.costs.items():
725
+ if key != 'total':
726
+ print(f"{key.replace('_', ' ').title()}: ${cost:.4f}")
727
+ print(f"\nTotal Cost: ${self.costs['total']:.4f}")
728
+
729
  class MentorEvaluator:
730
  """Main class for video evaluation"""
731
  def __init__(self, model_cache_dir: Optional[str] = None):
 
746
  self._feature_extractor = None
747
  self._content_analyzer = None
748
  self._recommendation_generator = None
749
+ self.cost_calculator = CostCalculator()
 
 
750
 
751
  @property
752
  def whisper_model(self):
 
756
  logger.info("Attempting to initialize Whisper model...")
757
  # First try to initialize model with downloading allowed
758
  self._whisper_model = WhisperModel(
759
+ "small",
760
  device="cpu",
761
  compute_type="int8",
762
  download_root=self.model_cache_dir,
 
769
  try:
770
  logger.info("Attempting to load model from local cache...")
771
  self._whisper_model = WhisperModel(
772
+ "small",
773
  device="cpu",
774
  compute_type="int8",
775
  download_root=self.model_cache_dir,
 
806
  return self._recommendation_generator
807
 
808
  def evaluate_video(self, video_path: str) -> Dict[str, Any]:
809
+ """Evaluate video with proper resource management and cost tracking"""
810
  with temporary_file(suffix=".wav") as temp_audio:
811
  try:
812
  # Extract audio
 
815
  tracker = ProgressTracker(status, progress_bar)
816
  self._extract_audio(video_path, temp_audio, tracker.update)
817
 
818
+ # Get audio duration for cost calculation
819
+ audio_info = sf.info(temp_audio)
820
+ duration_seconds = audio_info.duration
821
+ self.cost_calculator.add_transcription_cost(duration_seconds)
 
 
 
 
822
 
823
+ # Extract features and transcribe
824
+ audio_features = self.feature_extractor.extract_features(
825
+ temp_audio,
826
+ tracker.update
827
+ )
828
+ transcript = self._transcribe_audio(temp_audio, tracker.update)
829
+
830
+ # Analyze content with cost tracking
831
+ content_prompt = self.content_analyzer._create_analysis_prompt(transcript)
832
+ content_analysis = self.content_analyzer.analyze_content(transcript, tracker.update)
833
+ self.cost_calculator.add_gpt4_cost(
834
+ content_prompt,
835
+ json.dumps(content_analysis),
836
+ 'content_analysis'
837
+ )
838
 
839
+ # Evaluate speech metrics
840
+ speech_metrics = self._evaluate_speech_metrics(
841
+ transcript,
842
+ audio_features,
843
+ tracker.update
844
+ )
 
 
845
 
846
+ # Generate recommendations with cost tracking
847
+ rec_prompt = self.recommendation_generator._create_recommendation_prompt(
848
+ speech_metrics,
849
+ content_analysis
850
+ )
851
+ recommendations = self.recommendation_generator.generate_recommendations(
852
+ speech_metrics,
853
+ content_analysis,
854
+ tracker.update
855
+ )
856
+ self.cost_calculator.add_gpt4_cost(
857
+ rec_prompt,
858
+ json.dumps(recommendations),
859
+ 'recommendations'
860
+ )
861
 
862
+ # Print final cost breakdown
863
+ self.cost_calculator.print_total_cost()
 
 
 
 
 
 
 
864
 
865
  return {
866
  "communication": speech_metrics,
 
924
  raise AudioProcessingError(f"Audio extraction failed: {str(e)}")
925
 
926
  def _transcribe_audio(self, audio_path: str, progress_callback=None) -> str:
927
+ """Transcribe audio with optimized performance using batching and parallel processing"""
928
  try:
929
  if progress_callback:
930
  progress_callback(0.1, "Loading transcription model...")
931
+
932
+ # Check if GPU is available and set device accordingly
933
+ device = "cuda" if torch.cuda.is_available() else "cpu"
934
+ compute_type = "float16" if device == "cuda" else "int8"
935
+
936
+ # Generate cache key based on file content
937
+ cache_key = f"transcript_{hash(open(audio_path, 'rb').read())}"
938
+
939
+ # Check cache first
940
+ if cache_key in st.session_state:
941
+ logger.info("Using cached transcription")
942
+ return st.session_state[cache_key]
943
+
944
+ # Initialize model with optimized settings
945
+ model = WhisperModel(
946
+ "medium",
947
+ device=device,
948
+ compute_type=compute_type,
949
+ download_root=self.model_cache_dir,
950
+ local_files_only=False,
951
+ cpu_threads=4,
952
+ num_workers=2
953
+ )
954
+
955
+ if progress_callback:
956
+ progress_callback(0.2, "Starting transcription...")
957
+
958
+ # Get audio duration for progress calculation
959
  audio_info = sf.info(audio_path)
960
  total_duration = audio_info.duration
961
+
962
+ # First pass to count total segments
963
+ segments_preview, _ = model.transcribe(
964
+ audio_path,
965
+ beam_size=5,
966
+ word_timestamps=True,
967
+ vad_filter=True,
968
+ vad_parameters=dict(
969
+ min_silence_duration_ms=500,
970
+ speech_pad_ms=100
971
+ )
972
+ )
973
+ total_segments = sum(1 for _ in segments_preview)
974
+
975
+ def progress_updater(current_segment, segment_start, segment_duration):
976
+ """Callback function to update progress based on segment position"""
977
+ progress = min((segment_start + segment_duration) / total_duration, 1.0)
978
+ progress = 0.2 + (progress * 0.7) # Scale progress between 20% and 90%
979
+ if progress_callback:
980
+ time_remaining = ((total_duration - (segment_start + segment_duration)) /
981
+ (segment_start + segment_duration) *
982
+ (time.time() - start_time) if segment_start > 0 else 0)
983
+
984
+ status_message = (
985
+ f"Transcribing batch {current_segment}/{total_segments} "
986
+ f"({progress:.1%}) - "
987
+ f"ETA: {int(time_remaining)}s"
988
+ )
989
+ progress_callback(progress, status_message)
990
+
991
+ # Start timing for ETA calculation
992
+ start_time = time.time()
993
+
994
+ # Transcribe with progress updates
995
+ segments, _ = model.transcribe(
996
+ audio_path,
997
+ beam_size=5,
998
+ word_timestamps=True,
999
+ vad_filter=True,
1000
+ vad_parameters=dict(
1001
+ min_silence_duration_ms=500,
1002
+ speech_pad_ms=100
1003
+ )
1004
+ )
1005
+
1006
+ # Process segments and update progress
1007
+ transcript_parts = []
1008
+ for i, segment in enumerate(segments, 1):
1009
+ transcript_parts.append(segment.text)
1010
+ progress_updater(i, segment.start, segment.end - segment.start)
1011
+
1012
+ # Combine segments into final transcript
1013
+ transcript = ' '.join(transcript_parts)
1014
+
1015
+ # Cache the result
1016
+ st.session_state[cache_key] = transcript
1017
+
1018
  if progress_callback:
1019
  progress_callback(1.0, "Transcription complete!")
1020
+
1021
+ return transcript
1022
+
1023
  except Exception as e:
1024
  logger.error(f"Error in transcription: {e}")
1025
  raise
1026
 
1027
+ def _merge_transcripts(self, transcripts: List[str]) -> str:
1028
+ """Merge transcripts with overlap deduplication"""
1029
+ if not transcripts:
1030
+ return ""
1031
+
1032
+ def clean_text(text):
1033
+ # Remove extra spaces and normalize punctuation
1034
+ return ' '.join(text.split())
1035
+
1036
+ def find_overlap(text1, text2):
1037
+ # Find overlapping text between consecutive chunks
1038
+ words1 = text1.split()
1039
+ words2 = text2.split()
1040
+
1041
+ for i in range(min(len(words1), 20), 0, -1): # Check up to 20 words
1042
+ if ' '.join(words1[-i:]) == ' '.join(words2[:i]):
1043
+ return i
1044
+ return 0
1045
+
1046
+ merged = clean_text(transcripts[0])
1047
+
1048
+ for i in range(1, len(transcripts)):
1049
+ current = clean_text(transcripts[i])
1050
+ overlap_size = find_overlap(merged, current)
1051
+ merged += ' ' + current.split(' ', overlap_size)[-1]
1052
+
1053
+ return merged
1054
+
1055
  def calculate_speech_metrics(self, transcript: str, audio_duration: float) -> Dict[str, float]:
1056
  """Calculate words per minute and other speech metrics."""
1057
  words = len(transcript.split())
 
1372
 
1373
  recommendations = evaluation.get("recommendations", {})
1374
 
1375
+ # Calculate Overall Score
1376
+ communication_metrics = evaluation.get("communication", {})
1377
+ teaching_data = evaluation.get("teaching", {})
1378
+
1379
+ # Calculate Communication Score
1380
+ comm_scores = []
1381
+ for category in ["speed", "fluency", "flow", "intonation", "energy"]:
1382
+ if category in communication_metrics:
1383
+ if "score" in communication_metrics[category]:
1384
+ comm_scores.append(communication_metrics[category]["score"])
1385
+
1386
+ communication_score = (sum(comm_scores) / len(comm_scores) * 100) if comm_scores else 0
1387
+
1388
+ # Calculate Teaching Score (combining concept and code assessment)
1389
+ concept_assessment = teaching_data.get("Concept Assessment", {})
1390
+ code_assessment = teaching_data.get("Code Assessment", {})
1391
+
1392
+ teaching_scores = []
1393
+ # Add concept scores
1394
+ for category in concept_assessment.values():
1395
+ if isinstance(category, dict) and "Score" in category:
1396
+ teaching_scores.append(category["Score"])
1397
+
1398
+ # Add code scores
1399
+ for category in code_assessment.values():
1400
+ if isinstance(category, dict) and "Score" in category:
1401
+ teaching_scores.append(category["Score"])
1402
+
1403
+ teaching_score = (sum(teaching_scores) / len(teaching_scores) * 100) if teaching_scores else 0
1404
 
1405
+ # Calculate Overall Score (50-50 weight between communication and teaching)
1406
+ overall_score = (communication_score + teaching_score) / 2
1407
+
1408
+ # Display Overall Scores at the top of recommendations
1409
+ st.markdown("### 📊 Overall Performance")
1410
+ col1, col2, col3 = st.columns(3)
1411
+
1412
+ with col1:
1413
+ st.metric(
1414
+ "Communication Score",
1415
+ f"{communication_score:.1f}%",
1416
+ delta="Pass" if communication_score >= 70 else "Needs Improvement",
1417
+ delta_color="normal" if communication_score >= 70 else "inverse"
1418
+ )
1419
+
1420
+ with col2:
1421
+ st.metric(
1422
+ "Teaching Score",
1423
+ f"{teaching_score:.1f}%",
1424
+ delta="Pass" if teaching_score >= 70 else "Needs Improvement",
1425
+ delta_color="normal" if teaching_score >= 70 else "inverse"
1426
+ )
1427
+
1428
+ with col3:
1429
+ st.metric(
1430
+ "Overall Score",
1431
+ f"{overall_score:.1f}%",
1432
+ delta="Pass" if overall_score >= 70 else "Needs Improvement",
1433
+ delta_color="normal" if overall_score >= 70 else "inverse"
1434
+ )
1435
+
1436
+ # Continue with existing recommendations display
1437
  with st.expander("💡 Areas for Improvement", expanded=True):
1438
  improvements = recommendations.get("improvements", [])
1439
  if isinstance(improvements, list):
 
1714
 
1715
  return missing
1716
 
1717
+ def generate_pdf_report(evaluation_data: Dict[str, Any]) -> bytes:
1718
+ """Generate a formatted PDF report from evaluation data"""
1719
+ try:
1720
+ from reportlab.lib import colors
1721
+ from reportlab.lib.pagesizes import letter
1722
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
1723
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
1724
+ from io import BytesIO
1725
+
1726
+ # Create PDF buffer
1727
+ buffer = BytesIO()
1728
+ doc = SimpleDocTemplate(buffer, pagesize=letter)
1729
+ styles = getSampleStyleSheet()
1730
+ story = []
1731
+
1732
+ # Title
1733
+ title_style = ParagraphStyle(
1734
+ 'CustomTitle',
1735
+ parent=styles['Heading1'],
1736
+ fontSize=24,
1737
+ spaceAfter=30
1738
+ )
1739
+ story.append(Paragraph("Mentor Demo Evaluation Report", title_style))
1740
+ story.append(Spacer(1, 20))
1741
+
1742
+ # Communication Metrics Section
1743
+ story.append(Paragraph("Communication Metrics", styles['Heading2']))
1744
+ comm_metrics = evaluation_data.get("communication", {})
1745
+
1746
+ # Create tables for each metric category
1747
+ for category in ["speed", "fluency", "flow", "intonation", "energy"]:
1748
+ if category in comm_metrics:
1749
+ metrics = comm_metrics[category]
1750
+ story.append(Paragraph(category.title(), styles['Heading3']))
1751
+
1752
+ data = [[k.replace('_', ' ').title(), str(v)] for k, v in metrics.items()]
1753
+ t = Table(data, colWidths=[200, 200])
1754
+ t.setStyle(TableStyle([
1755
+ ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
1756
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
1757
+ ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
1758
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
1759
+ ('FONTSIZE', (0, 0), (-1, 0), 14),
1760
+ ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
1761
+ ('BACKGROUND', (0, 1), (-1, -1), colors.beige),
1762
+ ('TEXTCOLOR', (0, 1), (-1, -1), colors.black),
1763
+ ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
1764
+ ('FONTSIZE', (0, 1), (-1, -1), 12),
1765
+ ('GRID', (0, 0), (-1, -1), 1, colors.black)
1766
+ ]))
1767
+ story.append(t)
1768
+ story.append(Spacer(1, 20))
1769
+
1770
+ # Teaching Analysis Section
1771
+ story.append(Paragraph("Teaching Analysis", styles['Heading2']))
1772
+ teaching_data = evaluation_data.get("teaching", {})
1773
+
1774
+ for assessment_type in ["Concept Assessment", "Code Assessment"]:
1775
+ if assessment_type in teaching_data:
1776
+ story.append(Paragraph(assessment_type, styles['Heading3']))
1777
+ categories = teaching_data[assessment_type]
1778
+
1779
+ for category, details in categories.items():
1780
+ score = details.get("Score", 0)
1781
+ citations = details.get("Citations", [])
1782
+
1783
+ data = [
1784
+ [category, "Score: " + ("Pass" if score == 1 else "Needs Improvement")],
1785
+ ["Citations:", ""]
1786
+ ] + [["-", citation] for citation in citations]
1787
+
1788
+ t = Table(data, colWidths=[200, 300])
1789
+ t.setStyle(TableStyle([
1790
+ ('BACKGROUND', (0, 0), (-1, 0), colors.grey),
1791
+ ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
1792
+ ('ALIGN', (0, 0), (-1, -1), 'LEFT'),
1793
+ ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
1794
+ ('GRID', (0, 0), (-1, -1), 1, colors.black)
1795
+ ]))
1796
+ story.append(t)
1797
+ story.append(Spacer(1, 20))
1798
+
1799
+ # Recommendations Section
1800
+ story.append(Paragraph("Recommendations", styles['Heading2']))
1801
+ recommendations = evaluation_data.get("recommendations", {})
1802
+
1803
+ if "improvements" in recommendations:
1804
+ story.append(Paragraph("Areas for Improvement:", styles['Heading3']))
1805
+ for improvement in recommendations["improvements"]:
1806
+ story.append(Paragraph("• " + improvement, styles['Normal']))
1807
+
1808
+ # Build PDF
1809
+ doc.build(story)
1810
+ pdf_data = buffer.getvalue()
1811
+ buffer.close()
1812
+
1813
+ return pdf_data
1814
+
1815
+ except Exception as e:
1816
+ logger.error(f"Error generating PDF report: {e}")
1817
+ raise RuntimeError(f"Failed to generate PDF report: {str(e)}")
1818
+
1819
  def main():
1820
  try:
1821
  # Set page config must be the first Streamlit command
 
1824
  # Add custom CSS for animations and styling
1825
  st.markdown("""
1826
  <style>
1827
+ /* Shimmer animation keyframes */
1828
+ @keyframes shimmer {
1829
+ 0% {
1830
+ background-position: -1000px 0;
1831
+ }
1832
+ 100% {
1833
+ background-position: 1000px 0;
1834
+ }
1835
+ }
1836
+
1837
+ .title-shimmer {
1838
+ text-align: center;
1839
+ color: #1f77b4;
1840
+ position: relative;
1841
+ overflow: hidden;
1842
+ background: linear-gradient(
1843
+ 90deg,
1844
+ rgba(255, 255, 255, 0) 0%,
1845
+ rgba(255, 255, 255, 0.8) 50%,
1846
+ rgba(255, 255, 255, 0) 100%
1847
+ );
1848
+ background-size: 1000px 100%;
1849
+ animation: shimmer 3s infinite linear;
1850
+ }
1851
+
1852
+ /* Existing animations */
1853
  @keyframes fadeIn {
1854
+ from { opacity: 0; }
1855
+ to { opacity: 1; }
1856
  }
1857
 
1858
  @keyframes slideIn {
 
1866
  100% { transform: scale(1); }
1867
  }
1868
 
1869
+ .fade-in {
1870
+ animation: fadeIn 1s ease-in;
 
 
 
 
 
 
 
1871
  }
1872
 
1873
+ .slide-in {
1874
+ animation: slideIn 0.5s ease-out;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1875
  }
1876
 
1877
+ .pulse {
1878
+ animation: pulse 2s infinite;
1879
  }
1880
 
1881
  .metric-card {
1882
+ background-color: #f0f2f6;
 
 
1883
  border-radius: 10px;
1884
+ padding: 20px;
1885
+ margin: 10px 0;
1886
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
1887
+ transition: transform 0.3s ease;
1888
  }
1889
 
1890
+ .metric-card:hover {
1891
+ transform: translateY(-5px);
 
 
 
1892
  }
1893
 
1894
  .stButton>button {
 
 
 
 
 
 
1895
  transition: all 0.3s ease;
1896
  }
1897
 
1898
  .stButton>button:hover {
1899
+ transform: scale(1.05);
 
1900
  }
1901
 
1902
+ .category-header {
1903
+ background: linear-gradient(90deg, #1f77b4, #2c3e50);
1904
+ color: white;
1905
+ padding: 10px;
1906
+ border-radius: 5px;
1907
+ margin: 10px 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1908
  }
1909
 
1910
+ .score-badge {
1911
+ padding: 5px 10px;
1912
+ border-radius: 15px;
1913
+ font-weight: bold;
 
 
1914
  }
1915
 
1916
+ .score-pass {
1917
+ background-color: #28a745;
1918
+ color: white;
 
 
1919
  }
1920
 
1921
+ .score-fail {
1922
+ background-color: #dc3545;
1923
+ color: white;
 
 
1924
  }
1925
  </style>
1926
 
1927
  <div class="fade-in">
1928
+ <h1 class="title-shimmer">
1929
  🎓 Mentor Demo Review System
1930
  </h1>
1931
  </div>
1932
  """, unsafe_allow_html=True)
1933
 
1934
+ # Sidebar with instructions and status
1935
  with st.sidebar:
1936
  st.markdown("""
1937
+ <div class="slide-in">
1938
+ <h2>Instructions</h2>
1939
+ <ol>
1940
+ <li>Upload your teaching video</li>
1941
+ <li>Wait for the analysis</li>
1942
+ <li>Review the detailed feedback</li>
 
1943
  <li>Download the report</li>
1944
  </ol>
1945
  </div>
1946
  """, unsafe_allow_html=True)
1947
 
1948
+ # Add file format information separately
1949
+ st.markdown("**Supported formats:** MP4, AVI, MOV")
1950
+ st.markdown("**Maximum file size:** 500MB")
1951
+
1952
+ # Create a placeholder for status updates in the sidebar
1953
+ status_placeholder = st.empty()
1954
+ status_placeholder.info("Upload a video to begin analysis")
1955
 
1956
  # Check dependencies with progress
1957
  with st.status("Checking system requirements...") as status:
 
1977
  progress_bar.progress(1.0)
1978
  status.update(label="System requirements satisfied!", state="complete")
1979
 
1980
+ # Temporary: Add radio button for input type selection
1981
+ input_type = st.radio(
1982
+ "Select Input Type (Temporary Feature)",
1983
+ ["Video Only", "Video + Transcript"],
1984
+ help="Temporary feature: Choose to upload video only or video with transcript"
1985
+ )
1986
+
1987
  uploaded_file = st.file_uploader(
1988
+ "Upload Teaching Video",
1989
  type=['mp4', 'avi', 'mov'],
1990
  help="Upload your teaching video in MP4, AVI, or MOV format"
1991
  )
1992
+
1993
+ # Temporary: Add transcript uploader if Video + Transcript is selected
1994
+ uploaded_transcript = None
1995
+ if input_type == "Video + Transcript":
1996
+ uploaded_transcript = st.file_uploader(
1997
+ "Upload Transcript (Optional)",
1998
+ type=['txt'],
1999
+ help="Upload your transcript in TXT format"
2000
+ )
2001
 
2002
  if uploaded_file:
2003
+ # Update status in sidebar
2004
+ status_placeholder.info("Video uploaded, beginning processing...")
2005
+
2006
+ # Add a pulsing animation while processing
2007
  st.markdown("""
2008
+ <div class="pulse" style="text-align: center;">
2009
+ <h3>Processing your video...</h3>
 
2010
  </div>
2011
  """, unsafe_allow_html=True)
2012
 
 
2017
  try:
2018
  # Save uploaded file with progress
2019
  with st.status("Saving uploaded file...") as status:
2020
+ # Update sidebar status
2021
+ status_placeholder.info("Saving uploaded file...")
2022
  progress_bar = st.progress(0)
2023
 
2024
  # Save in chunks to show progress
 
2038
  status.update(label="File saved successfully!", state="complete")
2039
 
2040
  # Validate file size
2041
+ file_size = os.path.getsize(video_path) / (1024 * 1024 * 1024) # Size in GB
2042
+ if file_size > 2:
2043
+ st.error("File size exceeds 2GB limit. Please upload a smaller file.")
2044
  return
2045
 
2046
+ # Store evaluation results in session state
2047
  if 'evaluation_results' not in st.session_state:
2048
+ # Update sidebar status
2049
+ status_placeholder.info("Processing video and generating analysis...")
2050
+
2051
+ # Process video only if results aren't already in session state
2052
  with st.spinner("Processing video"):
2053
  evaluator = MentorEvaluator()
2054
+
2055
+ # Temporary: Handle transcript if provided
2056
+ if uploaded_transcript:
2057
+ transcript_text = uploaded_transcript.getvalue().decode('utf-8')
2058
+ # Extract audio features but skip transcription
2059
+ audio_features = evaluator.feature_extractor.extract_features(video_path)
2060
+
2061
+ # Evaluate speech metrics
2062
+ speech_metrics = evaluator._evaluate_speech_metrics(
2063
+ transcript_text,
2064
+ audio_features
2065
+ )
2066
+
2067
+ # Analyze content
2068
+ content_analysis = evaluator.content_analyzer.analyze_content(transcript_text)
2069
+
2070
+ # Generate recommendations
2071
+ recommendations = evaluator.recommendation_generator.generate_recommendations(
2072
+ speech_metrics,
2073
+ content_analysis
2074
+ )
2075
+
2076
+ # Combine results
2077
+ st.session_state.evaluation_results = {
2078
+ "communication": speech_metrics,
2079
+ "teaching": content_analysis,
2080
+ "recommendations": recommendations,
2081
+ "transcript": transcript_text
2082
+ }
2083
+ else:
2084
+ # Original flow: full video evaluation
2085
+ st.session_state.evaluation_results = evaluator.evaluate_video(video_path)
2086
 
2087
+ # Update sidebar status for completion
2088
+ status_placeholder.success("Analysis complete! Review results below.")
 
 
 
 
 
2089
 
2090
+ # Display results using stored evaluation
2091
+ st.success("Analysis complete!")
2092
  display_evaluation(st.session_state.evaluation_results)
 
2093
 
2094
+ # Add download options
2095
+ col1, col2 = st.columns(2)
2096
+
2097
+ with col1:
2098
+ if st.download_button(
2099
+ "📥 Download JSON Report",
2100
+ json.dumps(st.session_state.evaluation_results, indent=2),
2101
+ "evaluation_report.json",
2102
+ "application/json",
2103
+ help="Download the raw evaluation data in JSON format"
2104
+ ):
2105
+ st.success("JSON report downloaded successfully!")
2106
+
2107
+ with col2:
2108
+ if st.download_button(
2109
+ "📄 Download Full Report (PDF)",
2110
+ generate_pdf_report(st.session_state.evaluation_results),
2111
+ "evaluation_report.pdf",
2112
+ "application/pdf",
2113
+ help="Download a formatted PDF report with detailed analysis"
2114
+ ):
2115
+ st.success("PDF report downloaded successfully!")
2116
 
 
 
 
 
 
 
 
 
 
2117
  except Exception as e:
2118
+ # Update sidebar status for error
2119
+ status_placeholder.error(f"Error during processing: {str(e)}")
2120
  st.error(f"Error during evaluation: {str(e)}")
2121
+
2122
  finally:
2123
  # Clean up temp files
2124
  if 'temp_dir' in locals():