sparshmehta commited on
Commit
2984357
Β·
verified Β·
1 Parent(s): 1f91bc5

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +560 -0
app.py ADDED
@@ -0,0 +1,560 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ import numpy as np
4
+ import librosa
5
+ from moviepy.editor import VideoFileClip
6
+ import whisper
7
+ from openai import OpenAI
8
+ import tempfile
9
+ from scipy.signal import find_peaks
10
+ import gc
11
+ import warnings
12
+ import re
13
+ from contextlib import contextmanager
14
+
15
+ class CPUMentorEvaluator:
16
+ def __init__(self):
17
+ """Initialize the evaluator for CPU usage."""
18
+ self.api_key = st.secrets["OPENAI_API_KEY"]
19
+ if not self.api_key:
20
+ raise ValueError("OpenAI API key not found in secrets")
21
+
22
+ self.client = OpenAI(api_key=self.api_key)
23
+ self.whisper_model = None
24
+ self.accent_classifier = None
25
+
26
+ def _clear_memory(self):
27
+ """Clear memory and run garbage collection."""
28
+ if hasattr(self, 'whisper_model') and self.whisper_model is not None:
29
+ del self.whisper_model
30
+ self.whisper_model = None
31
+
32
+ if hasattr(self, 'accent_classifier') and self.accent_classifier is not None:
33
+ del self.accent_classifier
34
+ self.accent_classifier = None
35
+
36
+ gc.collect()
37
+
38
+ @contextmanager
39
+ def load_whisper_model(self):
40
+ """Load Whisper model with proper memory management."""
41
+ try:
42
+ self._clear_memory()
43
+ self.whisper_model = whisper.load_model("tiny", device="cpu")
44
+ yield self.whisper_model
45
+ finally:
46
+ if self.whisper_model is not None:
47
+ del self.whisper_model
48
+ self.whisper_model = None
49
+ gc.collect()
50
+
51
+ def extract_audio(self, video_path):
52
+ """Extract audio from video file with optimized settings."""
53
+ temp_audio = None
54
+ video = None
55
+ try:
56
+ self._clear_memory()
57
+ temp_audio = tempfile.NamedTemporaryFile(delete=False, suffix='.wav')
58
+ video = VideoFileClip(video_path, audio=True, target_resolution=(480,None), verbose=False)
59
+ video.audio.write_audiofile(temp_audio.name, fps=16000, verbose=False, logger=None)
60
+ return temp_audio.name
61
+ except Exception as e:
62
+ if temp_audio and os.path.exists(temp_audio.name):
63
+ os.unlink(temp_audio.name)
64
+ raise Exception(f"Audio extraction failed: {str(e)}")
65
+ finally:
66
+ if video:
67
+ video.close()
68
+ self._clear_memory()
69
+
70
+ def analyze_audio_features(self, audio_path):
71
+ """Analyze audio features with optimized memory usage for CPU."""
72
+ try:
73
+ CHUNK_SIZE = 60
74
+ duration = librosa.get_duration(path=audio_path)
75
+ num_chunks = int(np.ceil(duration / CHUNK_SIZE))
76
+
77
+ pitch_values = []
78
+ rms_values = []
79
+ spectral_centroids = []
80
+ spectral_rolloffs = []
81
+ mfccs_buffer = []
82
+
83
+ HOP_LENGTH = 512
84
+ N_FFT = 2048
85
+
86
+ for chunk_idx in range(num_chunks):
87
+ start_time = chunk_idx * CHUNK_SIZE
88
+ dur = min(CHUNK_SIZE, duration - start_time)
89
+
90
+ y, sr = librosa.load(audio_path, offset=start_time, duration=dur, sr=16000)
91
+
92
+ with warnings.catch_warnings():
93
+ warnings.simplefilter("ignore")
94
+
95
+ stft = librosa.stft(y, n_fft=N_FFT, hop_length=HOP_LENGTH)
96
+ S = np.abs(stft)
97
+
98
+ rms = np.sqrt(np.mean(S**2, axis=0))
99
+ rms_values.extend(rms)
100
+
101
+ f0, voiced_flag, _ = librosa.pyin(
102
+ y,
103
+ fmin=librosa.note_to_hz('C2'),
104
+ fmax=librosa.note_to_hz('C7'),
105
+ sr=sr,
106
+ frame_length=N_FFT,
107
+ hop_length=HOP_LENGTH,
108
+ fill_na=None
109
+ )
110
+ pitch_values.extend(f0[voiced_flag])
111
+
112
+ spectral_centroids.extend(librosa.feature.spectral_centroid(
113
+ S=S, sr=sr, hop_length=HOP_LENGTH)[0])
114
+ spectral_rolloffs.extend(librosa.feature.spectral_rolloff(
115
+ S=S, sr=sr, hop_length=HOP_LENGTH)[0])
116
+
117
+ mfcc = librosa.feature.mfcc(
118
+ y=y,
119
+ sr=sr,
120
+ n_mfcc=8,
121
+ n_fft=N_FFT,
122
+ hop_length=HOP_LENGTH
123
+ )
124
+ mfccs_buffer.append(mfcc)
125
+
126
+ del stft, S
127
+ gc.collect()
128
+
129
+ pitch_array = np.array(pitch_values)
130
+ rms_array = np.array(rms_values)
131
+ spectral_centroids = np.array(spectral_centroids)
132
+ spectral_rolloffs = np.array(spectral_rolloffs)
133
+
134
+ pitch_stats = {
135
+ 'mean': float(np.nanmean(pitch_array)),
136
+ 'std': float(np.nanstd(pitch_array)),
137
+ 'range': float(np.nanpercentile(pitch_array, 95) -
138
+ np.nanpercentile(pitch_array, 5))
139
+ }
140
+
141
+ silence_threshold = np.mean(rms_array) * 0.1
142
+ silent_frames = rms_array < silence_threshold
143
+ frame_time = HOP_LENGTH / sr
144
+ pause_stats = self._analyze_pauses(silent_frames, frame_time)
145
+
146
+ result = {
147
+ 'pitch_analysis': {
148
+ 'statistics': pitch_stats,
149
+ 'patterns': {
150
+ 'rising_count': int(np.sum(np.diff(pitch_values) > 20)),
151
+ 'falling_count': int(np.sum(np.diff(pitch_values) < -20))
152
+ }
153
+ },
154
+ 'voice_quality': {
155
+ 'spectral_centroid_mean': float(np.mean(spectral_centroids)),
156
+ 'spectral_rolloff_mean': float(np.mean(spectral_rolloffs)),
157
+ 'mfcc_stats': {
158
+ 'mean': np.mean(np.concatenate(mfccs_buffer, axis=1), axis=1).tolist(),
159
+ 'std': np.std(np.concatenate(mfccs_buffer, axis=1), axis=1).tolist()
160
+ }
161
+ },
162
+ 'rhythm_analysis': {
163
+ 'pause_stats': pause_stats,
164
+ 'tempo': float(librosa.beat.tempo(onset_envelope=librosa.onset.onset_strength(
165
+ y=librosa.load(audio_path, duration=30, sr=16000)[0],
166
+ sr=16000
167
+ ))[0])
168
+ },
169
+ 'energy_dynamics': {
170
+ 'rms_energy_mean': float(np.mean(rms_values)),
171
+ 'rms_energy_std': float(np.std(rms_values)),
172
+ 'energy_range': float(np.percentile(rms_values, 95) -
173
+ np.percentile(rms_values, 5))
174
+ }
175
+ }
176
+
177
+ del pitch_array, rms_array, spectral_centroids, spectral_rolloffs
178
+ gc.collect()
179
+
180
+ return result
181
+
182
+ except Exception as e:
183
+ raise Exception(f"Audio analysis failed: {str(e)}")
184
+ finally:
185
+ self._clear_memory()
186
+
187
+ def _analyze_pauses(self, silent_frames, frame_time):
188
+ """Analyze pauses with minimal memory usage."""
189
+ pause_durations = []
190
+ current_pause = 0
191
+
192
+ for is_silent in silent_frames:
193
+ if is_silent:
194
+ current_pause += 1
195
+ elif current_pause > 0:
196
+ duration = current_pause * frame_time
197
+ if duration > 0.3: # Only count pauses longer than 300ms
198
+ pause_durations.append(duration)
199
+ current_pause = 0
200
+
201
+ if pause_durations:
202
+ return {
203
+ 'total_pauses': len(pause_durations),
204
+ 'mean_pause_duration': float(np.mean(pause_durations))
205
+ }
206
+ return {
207
+ 'total_pauses': 0,
208
+ 'mean_pause_duration': 0.0
209
+ }
210
+
211
+ def calculate_speech_metrics(self, transcript, audio_duration):
212
+ """Calculate words per minute and other speech metrics."""
213
+ words = len(transcript.split())
214
+ minutes = audio_duration / 60
215
+ return {
216
+ 'words_per_minute': words / minutes if minutes > 0 else 0,
217
+ 'total_words': words,
218
+ 'duration_minutes': minutes
219
+ }
220
+
221
+ def _analyze_voice_quality(self, transcript, audio_features):
222
+ """Analyze voice quality aspects."""
223
+ try:
224
+ prompt = f"""Analyze the following voice metrics for teaching quality:
225
+
226
+ Transcript excerpt: {transcript[:1000]}...
227
+
228
+ Voice Metrics:
229
+ - Pitch Mean: {audio_features['pitch_analysis']['statistics']['mean']:.1f}Hz
230
+ - Pitch Variation: {audio_features['pitch_analysis']['statistics']['std']:.1f}Hz
231
+ - Energy Dynamics: {audio_features['energy_dynamics']['rms_energy_mean']:.2f}
232
+
233
+ Evaluate voice quality focusing on:
234
+ 1. Clarity and projection
235
+ 2. Emotional engagement
236
+ 3. Professional tone
237
+ """
238
+ response = self.client.chat.completions.create(
239
+ model="gpt-4",
240
+ messages=[
241
+ {"role": "system", "content": "You are an expert in voice analysis."},
242
+ {"role": "user", "content": prompt}
243
+ ],
244
+ max_tokens=500
245
+ )
246
+ return response.choices[0].message.content
247
+ except Exception as e:
248
+ return f"Voice quality analysis failed: {str(e)}"
249
+
250
+ def _analyze_teaching_content(self, transcript):
251
+ """Analyze teaching content for accuracy, principles, and examples."""
252
+ try:
253
+ prompt = f"""Analyze this teaching transcript for:
254
+ 1. Subject Matter Accuracy:
255
+ - Identify any factual errors, wrong assumptions, or incorrect correlations
256
+ - Rate accuracy on a scale of 0-1
257
+ 2. First Principles Approach:
258
+ - Evaluate if concepts are built from fundamentals before introducing technical terms
259
+ - Rate approach on a scale of 0-1
260
+ 3. Examples and Business Context:
261
+ - Assess use of business examples and practical context
262
+ - Rate contextual relevance on a scale of 0-1
263
+
264
+ Transcript: {transcript}...
265
+
266
+ Provide specific citations for any identified issues.
267
+ """
268
+ response = self.client.chat.completions.create(
269
+ model="gpt-4",
270
+ messages=[
271
+ {"role": "system", "content": "You are an expert in pedagogical assessment."},
272
+ {"role": "user", "content": prompt}
273
+ ],
274
+ max_tokens=500
275
+ )
276
+ return response.choices[0].message.content
277
+ except Exception as e:
278
+ return f"Teaching content analysis failed: {str(e)}"
279
+
280
+ def _analyze_code_explanation(self, transcript):
281
+ """Analyze code explanation quality."""
282
+ try:
283
+ prompt = f"""Analyze the code explanation in this transcript for:
284
+ 1. Depth of Explanation:
285
+ - Evaluate coverage of syntax, libraries, functions, and methods
286
+ - Rate depth on a scale of 0-1
287
+ 2. Output Interpretation:
288
+ - Assess business context interpretation of results
289
+ - Rate interpretation on a scale of 0-1
290
+ 3. Complexity Breakdown:
291
+ - Evaluate explanation of code modules and logical flow
292
+ - Rate breakdown quality on a scale of 0-1
293
+
294
+ Transcript: {transcript}...
295
+
296
+ Provide specific citations for any identified issues.
297
+ """
298
+ response = self.client.chat.completions.create(
299
+ model="gpt-4",
300
+ messages=[
301
+ {"role": "system", "content": "You are an expert in code review and teaching."},
302
+ {"role": "user", "content": prompt}
303
+ ],
304
+ max_tokens=500
305
+ )
306
+ return response.choices[0].message.content
307
+ except Exception as e:
308
+ return f"Code explanation analysis failed: {str(e)}"
309
+
310
+ def generate_enhanced_report(self, video_path):
311
+ """Generate structured evaluation report."""
312
+ audio_path = None
313
+ try:
314
+ audio_path = self.extract_audio(video_path)
315
+
316
+ with self.load_whisper_model() as model:
317
+ result = model.transcribe(audio_path)
318
+ transcript = result["text"]
319
+
320
+ audio_features = self.analyze_audio_features(audio_path)
321
+ audio_duration = librosa.get_duration(path=audio_path)
322
+ speech_metrics = self.calculate_speech_metrics(transcript, audio_duration)
323
+
324
+ wpm = speech_metrics['words_per_minute']
325
+ wpm_score = 1 if 120 <= wpm <= 160 else 0
326
+
327
+ filler_words = len(re.findall(r'\b(um|uh|like|you know|basically)\b', transcript.lower()))
328
+ fpm = (filler_words / speech_metrics['duration_minutes'])
329
+
330
+ ppm = audio_features['rhythm_analysis']['pause_stats']['total_pauses'] / speech_metrics['duration_minutes']
331
+ pause_score = 1 if 2 <= ppm <= 8 else 0
332
+
333
+ energy_values = audio_features['energy_dynamics']
334
+ energy_summary = {
335
+ 'min': np.percentile([energy_values['rms_energy_mean']], 0),
336
+ 'q1': np.percentile([energy_values['rms_energy_mean']], 25),
337
+ 'median': np.percentile([energy_values['rms_energy_mean']], 50),
338
+ 'q3': np.percentile([energy_values['rms_energy_mean']], 75),
339
+ 'max': np.percentile([energy_values['rms_energy_mean']], 100)
340
+ }
341
+
342
+ teaching_analysis = self._analyze_teaching_content(transcript)
343
+ code_analysis = self._analyze_code_explanation(transcript)
344
+ voice_quality = self._analyze_voice_quality(transcript, audio_features)
345
+
346
+ intonation_score = 1 if (audio_features['pitch_analysis']['patterns']['rising_count'] +
347
+ audio_features['pitch_analysis']['patterns']['falling_count']) / speech_metrics['duration_minutes'] > 5 else 0
348
+
349
+ energy_score = 1 if (energy_values['rms_energy_std'] / energy_values['rms_energy_mean']) > 0.2 else 0
350
+
351
+ report = f"""REPORT
352
+
353
+ 1. COMMUNICATION
354
+ 1. Speech Speed:
355
+ - Words per Minute: {wpm:.1f}
356
+ - Score: {wpm_score} (Acceptable range: 120-160 WPM)
357
+
358
+ 2. Voice Quality:
359
+ {voice_quality}
360
+
361
+ 3. Fluency:
362
+ - Fillers per Minute: {fpm:.1f}
363
+ - Score: {1 if fpm < 3 else 0}
364
+
365
+ 4. Break/Flow:
366
+ - Pauses per Minute: {ppm:.1f}
367
+ - Score: {pause_score}
368
+
369
+ 5. Intonation:
370
+ - Rising patterns: {audio_features['pitch_analysis']['patterns']['rising_count']}
371
+ - Falling patterns: {audio_features['pitch_analysis']['patterns']['falling_count']}
372
+ - Score: {intonation_score}
373
+
374
+ 6. Energy:
375
+ Five-point summary:
376
+ - Min: {energy_summary['min']:.2f}
377
+ - Q1: {energy_summary['q1']:.2f}
378
+ - Median: {energy_summary['median']:.2f}
379
+ - Q3: {energy_summary['q3']:.2f}
380
+ - Max: {energy_summary['max']:.2f}
381
+ - Score: {energy_score}
382
+
383
+ 2. TEACHING
384
+ 1. Content Analysis:
385
+ {teaching_analysis}
386
+
387
+ 2. Code Explanation:
388
+ {code_analysis}
389
+
390
+ Full Transcript:
391
+ {transcript}
392
+ """
393
+ return report
394
+
395
+ except Exception as e:
396
+ raise Exception(f"Report generation failed: {str(e)}")
397
+ finally:
398
+ if audio_path and os.path.exists(audio_path):
399
+ os.unlink(audio_path)
400
+ self._clear_memory()
401
+
402
+ def create_temp_directory():
403
+ """Create a temporary directory for file processing."""
404
+ temp_dir = tempfile.mkdtemp()
405
+ return temp_dir
406
+
407
+ def main():
408
+ st.set_page_config(
409
+ page_title="Mentor Speech Evaluator",
410
+ page_icon="πŸŽ“",
411
+ layout="wide"
412
+ )
413
+
414
+ st.title("πŸŽ“ Mentor Speech Analysis Tool")
415
+
416
+ # Add custom CSS
417
+ st.markdown("""
418
+ <style>
419
+ .stProgress > div > div > div > div {
420
+ background-color: #1f77b4;
421
+ }
422
+ .metric-card {
423
+ background-color: #f8f9fa;
424
+ padding: 20px;
425
+ border-radius: 10px;
426
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
427
+ }
428
+ </style>
429
+ """, unsafe_allow_html=True)
430
+
431
+ st.markdown("""
432
+ This tool analyzes teaching videos and provides detailed feedback on:
433
+ - Communication quality
434
+ - Speech patterns
435
+ - Teaching effectiveness
436
+ - Code explanation clarity
437
+ """)
438
+
439
+ # Initialize session state
440
+ if 'analysis_complete' not in st.session_state:
441
+ st.session_state.analysis_complete = False
442
+ if 'report_data' not in st.session_state:
443
+ st.session_state.report_data = None
444
+
445
+ # File uploader
446
+ uploaded_file = st.file_uploader("Upload a video file", type=['mp4', 'avi', 'mov', 'mkv'])
447
+
448
+ if uploaded_file:
449
+ try:
450
+ if not st.session_state.analysis_complete:
451
+ # Create progress bar and status
452
+ progress_bar = st.progress(0)
453
+ status_text = st.empty()
454
+
455
+ # Save uploaded file temporarily
456
+ temp_dir = create_temp_directory()
457
+ temp_video_path = os.path.join(temp_dir, uploaded_file.name)
458
+
459
+ with open(temp_video_path, 'wb') as f:
460
+ f.write(uploaded_file.getbuffer())
461
+
462
+ status_text.text("Initializing analysis...")
463
+ progress_bar.progress(10)
464
+
465
+ # Initialize evaluator
466
+ evaluator = CPUMentorEvaluator()
467
+ status_text.text("Processing video...")
468
+ progress_bar.progress(30)
469
+
470
+ # Generate report
471
+ report = evaluator.generate_enhanced_report(temp_video_path)
472
+ st.session_state.report_data = report
473
+ st.session_state.analysis_complete = True
474
+ progress_bar.progress(100)
475
+ status_text.text("Analysis complete!")
476
+
477
+ # Display results
478
+ if st.session_state.analysis_complete and st.session_state.report_data:
479
+ report = st.session_state.report_data
480
+
481
+ # Split report into sections
482
+ sections = report.split('\n\n')
483
+
484
+ # Create tabs for different aspects of analysis
485
+ tab1, tab2, tab3 = st.tabs(["Communication", "Teaching", "Transcript"])
486
+
487
+ with tab1:
488
+ st.subheader("πŸ’¬ Communication Analysis")
489
+ communication_metrics = sections[2] if len(sections) > 2 else "Analysis not available"
490
+
491
+ # Create metrics display using columns
492
+ cols = st.columns(3)
493
+
494
+ # Extract and display key metrics
495
+ if "Words per Minute:" in communication_metrics:
496
+ wpm = float(re.search(r"Words per Minute: (\d+\.?\d*)", communication_metrics).group(1))
497
+ cols[0].metric("Speech Speed (WPM)", f"{wpm:.1f}")
498
+
499
+ if "Fillers per Minute:" in communication_metrics:
500
+ fpm = float(re.search(r"Fillers per Minute: (\d+\.?\d*)", communication_metrics).group(1))
501
+ cols[1].metric("Filler Words (per min)", f"{fpm:.1f}")
502
+
503
+ if "Pauses per Minute:" in communication_metrics:
504
+ ppm = float(re.search(r"Pauses per Minute: (\d+\.?\d*)", communication_metrics).group(1))
505
+ cols[2].metric("Pauses (per min)", f"{ppm:.1f}")
506
+
507
+ st.markdown(communication_metrics)
508
+
509
+ with tab2:
510
+ st.subheader("πŸ“š Teaching Analysis")
511
+ teaching_metrics = sections[3] if len(sections) > 3 else "Analysis not available"
512
+ st.markdown(teaching_metrics)
513
+
514
+ with tab3:
515
+ st.subheader("πŸ“ Full Transcript")
516
+ transcript_section = sections[-1] if len(sections) > 4 else "Transcript not available"
517
+ st.markdown(transcript_section)
518
+
519
+ # Download button for full report
520
+ st.download_button(
521
+ label="πŸ“₯ Download Full Report",
522
+ data=report,
523
+ file_name="mentor_analysis_report.txt",
524
+ mime="text/plain",
525
+ key="download_report"
526
+ )
527
+
528
+ except Exception as e:
529
+ st.error(f"An error occurred: {str(e)}")
530
+
531
+ finally:
532
+ # Cleanup
533
+ if 'temp_dir' in locals() and os.path.exists(temp_dir):
534
+ import shutil
535
+ shutil.rmtree(temp_dir)
536
+ gc.collect()
537
+
538
+ # Sidebar
539
+ with st.sidebar:
540
+ st.markdown("""
541
+ ### About
542
+ This tool uses advanced AI to analyze teaching videos and provide feedback on:
543
+ - Speech speed and clarity
544
+ - Voice quality and engagement
545
+ - Teaching effectiveness
546
+ - Code explanation quality
547
+
548
+ ### Usage Tips
549
+ 1. Upload a video file (MP4, AVI, MOV, or MKV)
550
+ 2. Wait for the analysis to complete
551
+ 3. View results in organized sections
552
+ 4. Download the full report for detailed feedback
553
+
554
+ ### Privacy Note
555
+ All uploaded videos are processed securely and deleted immediately after analysis.
556
+ No data is stored permanently.
557
+ """)
558
+
559
+ if __name__ == "__main__":
560
+ main()