ferhatbou commited on
Commit
5cdbd8d
Β·
1 Parent(s): 9aa62c0

Fix import issue

Browse files
Files changed (3) hide show
  1. app.py +159 -32
  2. requirements.txt +10 -9
  3. video_accent_analyzer.py +160 -62
app.py CHANGED
@@ -1,47 +1,174 @@
1
  import gradio as gr
2
  from video_accent_analyzer import VideoAccentAnalyzer
 
 
3
 
4
  analyzer = VideoAccentAnalyzer()
5
 
6
 
7
- def analyze_video(url=None, video_file=None):
8
- if url:
9
- result = analyzer.analyze_video_url(url)
10
- elif video_file:
11
- result = analyzer.analyze_local_video(video_file)
12
- else:
13
- return "❌ Please provide a video URL or upload a file"
14
 
15
- if 'error' in result:
16
- return f"❌ Error: {result['error']}"
17
 
18
- markdown = f"## 🎯 Results\n"
19
- markdown += f"**Predicted Accent:** {result['predicted_accent']}\n"
20
- markdown += f"**Confidence:** {result['confidence']:.1f}%\n"
21
- markdown += f"**English Confidence:** {result['english_confidence']:.1f}%\n\n"
22
- markdown += "### πŸ“Š Probability Breakdown:\n"
 
 
 
 
23
 
24
- for accent, prob in sorted(result['probabilities'].items(), key=lambda x: x[1], reverse=True):
25
- markdown += f"- {accent}: {prob:.1f}%\n"
 
 
 
 
 
26
 
27
- return markdown
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
 
30
  # Create Gradio interface
31
- interface = gr.Interface(
32
- fn=analyze_video,
33
- inputs=[
34
- gr.Textbox(label="Video URL (YouTube/Loom/Direct MP4)"),
35
- gr.File(label="Or Upload Video File", type="filepath")
36
- ],
37
- outputs=gr.Markdown(label="Analysis Results"),
38
- examples=[
39
- ["https://www.youtube.com/watch?v=abc123 ", None],
40
- [None, "example_video.mp4"]
41
- ],
42
- title="🎧 Video Accent Analyzer",
43
- description="Detect English accents in videos from YouTube, Loom, or uploaded files"
44
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  if __name__ == "__main__":
47
- interface.launch()
 
1
  import gradio as gr
2
  from video_accent_analyzer import VideoAccentAnalyzer
3
+ import plotly.graph_objects as go
4
+ import pandas as pd
5
 
6
  analyzer = VideoAccentAnalyzer()
7
 
8
 
9
+ def create_plotly_chart(probabilities):
10
+ """Create an interactive Plotly bar chart for accent probabilities"""
11
+ accents = [analyzer.accent_display_names.get(acc, acc.title()) for acc in probabilities.keys()]
12
+ probs = list(probabilities.values())
 
 
 
13
 
14
+ colors = ['#4CAF50' if p == max(probs) else '#2196F3' if p >= 20
15
+ else '#FFC107' if p >= 10 else '#9E9E9E' for p in probs]
16
 
17
+ fig = go.Figure(data=[
18
+ go.Bar(
19
+ x=accents,
20
+ y=probs,
21
+ marker_color=colors,
22
+ text=[f'{p:.1f}%' for p in probs],
23
+ textposition='auto',
24
+ )
25
+ ])
26
 
27
+ fig.update_layout(
28
+ title='Accent Probability Distribution',
29
+ xaxis_title='Accent Type',
30
+ yaxis_title='Probability (%)',
31
+ template='plotly_white',
32
+ yaxis_range=[0, 100],
33
+ )
34
 
35
+ return fig
36
+
37
+
38
+ def analyze_video(url=None, video_file=None, duration=30):
39
+ """Analyze video from URL or file with enhanced output"""
40
+ try:
41
+ if not url and not video_file:
42
+ return (
43
+ "### ❌ Error\nPlease provide either a video URL or upload a video file.",
44
+ None
45
+ )
46
+
47
+ if url:
48
+ result = analyzer.analyze_video_url(url, max_duration=duration)
49
+ else:
50
+ result = analyzer.analyze_local_video(video_file, max_duration=duration)
51
+
52
+ if 'error' in result:
53
+ return (
54
+ f"### ❌ Error\n{result['error']}",
55
+ None
56
+ )
57
+
58
+ # Create markdown output
59
+ markdown = f"""
60
+ ### 🎯 Analysis Results
61
+
62
+ **Primary Classification:**
63
+ - πŸ—£οΈ Predicted Accent: {analyzer.accent_display_names.get(result['predicted_accent'])}
64
+ - πŸ“Š Confidence: {result['accent_confidence']:.1f}%
65
+ - 🌍 English Confidence: {result['english_confidence']:.1f}%
66
+
67
+ **Audio Analysis:**
68
+ - ⏱️ Duration: {result['audio_duration']:.1f} seconds
69
+ - πŸ“Š Quality Score: {result.get('audio_quality_score', 'N/A')}
70
+ - 🎡 Chunks Analyzed: {result.get('chunks_analyzed', 1)}
71
+
72
+ **Assessment:**
73
+ - {'βœ… Strong English Speaker' if result['english_confidence'] >= 70 else '⚠️ Moderate English Confidence' if result['english_confidence'] >= 50 else '❓ Low English Confidence'}
74
+ - {'🎯 High Accent Confidence' if result['accent_confidence'] >= 70 else 'πŸ€” Moderate Accent Confidence' if result['accent_confidence'] >= 50 else '❓ Low Accent Confidence'}
75
+ """
76
+
77
+ # Create visualization
78
+ fig = create_plotly_chart(result['all_probabilities'])
79
+
80
+ return markdown, fig
81
+
82
+ except Exception as e:
83
+ return f"### ❌ Error\nAn unexpected error occurred: {str(e)}", None
84
 
85
 
86
  # Create Gradio interface
87
+ css = """
88
+ .gradio-container {
89
+ font-family: 'IBM Plex Sans', sans-serif;
90
+ }
91
+ .gr-button {
92
+ background: linear-gradient(45deg, #4CAF50, #2196F3);
93
+ border: none;
94
+ }
95
+ .gr-button:hover {
96
+ background: linear-gradient(45deg, #2196F3, #4CAF50);
97
+ transform: scale(1.02);
98
+ }
99
+ """
100
+
101
+ with gr.Blocks(css=css) as interface:
102
+ gr.Markdown("""
103
+ # 🎧 Video Accent Analyzer
104
+
105
+ Analyze English accents in videos from various sources:
106
+ - YouTube videos
107
+ - Loom recordings
108
+ - Direct video links
109
+ - Uploaded video files
110
+
111
+ ### πŸ’‘ Tips
112
+ - Keep videos under 2 minutes for best results
113
+ - Ensure clear audio quality
114
+ - Multiple speakers may affect accuracy
115
+ """)
116
+
117
+ with gr.Row():
118
+ with gr.Column():
119
+ url_input = gr.Textbox(
120
+ label="Video URL",
121
+ placeholder="Enter YouTube, Loom, or direct video URL"
122
+ )
123
+ video_input = gr.File(
124
+ label="Or Upload Video",
125
+ file_types=["video"]
126
+ )
127
+ duration = gr.Slider(
128
+ minimum=10,
129
+ maximum=120,
130
+ value=30,
131
+ step=10,
132
+ label="Maximum Duration (seconds)"
133
+ )
134
+ analyze_btn = gr.Button("πŸ” Analyze Video", variant="primary")
135
+
136
+ with gr.Column():
137
+ output_text = gr.Markdown(label="Analysis Results")
138
+ output_plot = gr.Plot(label="Accent Distribution")
139
+
140
+ analyze_btn.click(
141
+ fn=analyze_video,
142
+ inputs=[url_input, video_input, duration],
143
+ outputs=[output_text, output_plot]
144
+ )
145
+
146
+ gr.Examples(
147
+ examples=[
148
+ ["https://www.youtube.com/watch?v=NO5SbsvIjHE", None, 30],
149
+ ["https://www.youtube.com/watch?v=YQHsXMglC9A", None, 30],
150
+ ],
151
+ inputs=[url_input, video_input, duration],
152
+ outputs=[output_text, output_plot],
153
+ label="Example Videos"
154
+ )
155
+
156
+ # Add requirements.txt
157
+ requirements = """
158
+ gradio>=4.0.0
159
+ plotly>=5.0.0
160
+ yt-dlp
161
+ librosa
162
+ soundfile
163
+ transformers
164
+ torch
165
+ ffmpeg-python
166
+ matplotlib
167
+ seaborn
168
+ """
169
+
170
+ with open("requirements.txt", "w") as f:
171
+ f.write(requirements)
172
 
173
  if __name__ == "__main__":
174
+ interface.launch()
requirements.txt CHANGED
@@ -1,9 +1,10 @@
1
- yt-dlp
2
- librosa
3
- soundfile
4
- transformers
5
- torch
6
- gradio
7
- ffmpeg-python
8
- matplotlib
9
- seaborn
 
 
1
+ gradio>=4.0.0
2
+ plotly>=5.0.0
3
+ yt-dlp
4
+ librosa
5
+ soundfile
6
+ transformers
7
+ torch
8
+ ffmpeg-python
9
+ matplotlib
10
+ seaborn
video_accent_analyzer.py CHANGED
@@ -1,3 +1,8 @@
 
 
 
 
 
1
 
2
  import os
3
  import sys
@@ -17,7 +22,6 @@ import seaborn as sns
17
  # Suppress warnings for cleaner output
18
  warnings.filterwarnings('ignore')
19
 
20
-
21
  def install_if_missing(packages):
22
  """Install packages if they're not already available in Kaggle"""
23
  for package in packages:
@@ -30,7 +34,6 @@ def install_if_missing(packages):
30
  print(f"Installing {package}...")
31
  subprocess.check_call([sys.executable, "-m", "pip", "install", package, "--quiet"])
32
 
33
-
34
  # Required packages for Kaggle
35
  required_packages = [
36
  "yt-dlp",
@@ -52,7 +55,6 @@ import librosa
52
  import soundfile as sf
53
  import yt_dlp
54
 
55
-
56
  class VideoAccentAnalyzer:
57
  def __init__(self, model_name="dima806/multiple_accent_classification"):
58
  """Initialize the accent analyzer for Kaggle environment"""
@@ -101,6 +103,21 @@ class VideoAccentAnalyzer:
101
 
102
  return True, url
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  def download_video(self, url, max_duration=None):
105
  """Download video using yt-dlp with improved error handling"""
106
  is_valid, result = self._validate_url(url)
@@ -111,55 +128,106 @@ class VideoAccentAnalyzer:
111
  url = result
112
  output_path = os.path.join(self.temp_dir, "video.%(ext)s")
113
 
 
114
  ydl_opts = {
115
  'outtmpl': output_path,
116
- 'format': 'best[height<=720]/best', # Limit quality for faster download
117
- 'quiet': True,
118
- 'no_warnings': True,
119
- 'socket_timeout': 30,
120
- 'retries': 3,
 
 
 
 
 
121
  }
122
 
123
  if max_duration:
124
- ydl_opts['match_filter'] = lambda info: None if info.get('duration',
125
- 0) <= max_duration * 2 else "Video too long"
126
 
127
  try:
128
  with yt_dlp.YoutubeDL(ydl_opts) as ydl:
129
  print(f"πŸ“₯ Downloading video from: {url}")
130
  start_time = time.time()
 
 
 
 
 
 
 
 
 
131
  ydl.download([url])
132
  download_time = time.time() - start_time
133
 
134
- # Find downloaded file
 
135
  for file in os.listdir(self.temp_dir):
136
- if file.startswith("video."):
137
- video_path = os.path.join(self.temp_dir, file)
138
- if self._is_valid_video(video_path):
139
- print(f"βœ… Downloaded valid video: {file} ({download_time:.1f}s)")
140
- return video_path
 
 
 
 
141
  else:
142
- print("❌ Downloaded file is not a valid video")
143
- return None
 
 
 
 
 
 
 
 
144
 
145
  except Exception as e:
146
  print(f"⚠️ yt-dlp failed: {e}")
147
  return self._try_direct_download(url)
148
 
149
  def _is_valid_video(self, file_path):
150
- """Verify video file has valid structure"""
151
  try:
 
 
 
 
 
152
  result = subprocess.run(
153
- ['ffprobe', '-v', 'error', '-show_format', '-show_streams', file_path],
154
- capture_output=True, text=True, timeout=10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
  )
156
- return result.returncode == 0
 
 
157
  except subprocess.TimeoutExpired:
158
- print("⚠️ Video validation timed out")
159
- return False
160
  except Exception as e:
161
- print(f"⚠️ Video validation error: {e}")
162
- return False
163
 
164
  def _try_direct_download(self, url):
165
  """Enhanced fallback for direct video URLs"""
@@ -186,7 +254,7 @@ class VideoAccentAnalyzer:
186
  f.write(chunk)
187
  file_size += len(chunk)
188
 
189
- print(f"πŸ“ Downloaded {file_size / (1024 * 1024):.1f} MB")
190
 
191
  if self._is_valid_video(video_path):
192
  print("βœ… Direct download successful")
@@ -203,8 +271,9 @@ class VideoAccentAnalyzer:
203
  """Extract audio with improved error handling and progress"""
204
  audio_path = os.path.join(self.temp_dir, "audio.wav")
205
 
 
206
  cmd = ['ffmpeg', '-i', video_path, '-vn', '-acodec', 'pcm_s16le',
207
- '-ar', '16000', '-ac', '1', '-y', '-loglevel', 'error']
208
 
209
  if max_duration:
210
  cmd.extend(['-t', str(max_duration)])
@@ -213,15 +282,43 @@ class VideoAccentAnalyzer:
213
  try:
214
  print(f"🎡 Extracting audio (max {max_duration}s)...")
215
  start_time = time.time()
216
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
 
 
217
  extraction_time = time.time() - start_time
218
 
219
- if result.returncode == 0 and os.path.exists(audio_path):
220
- file_size = os.path.getsize(audio_path) / (1024 * 1024)
221
  print(f"βœ… Audio extracted successfully ({extraction_time:.1f}s, {file_size:.1f}MB)")
222
  return audio_path
223
  else:
224
- raise Exception(f"FFmpeg error: {result.stderr}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
  except subprocess.TimeoutExpired:
227
  print("❌ Audio extraction timed out")
@@ -366,7 +463,7 @@ class VideoAccentAnalyzer:
366
  return {"error": f"File not found: {file_path}"}
367
 
368
  # Check file size
369
- file_size = os.path.getsize(file_path) / (1024 * 1024) # MB
370
  print(f"πŸ“ File size: {file_size:.1f} MB")
371
 
372
  video_filename = os.path.basename(file_path)
@@ -394,8 +491,7 @@ class VideoAccentAnalyzer:
394
  def display_results(self, results):
395
  """Enhanced results display with visualizations"""
396
  if 'error' in results:
397
- display(HTML(
398
- f"<div style='color: red; font-size: 16px; padding: 10px; border: 1px solid red; border-radius: 5px;'>❌ {results['error']}</div>"))
399
  return
400
 
401
  accent = results['predicted_accent']
@@ -411,20 +507,20 @@ class VideoAccentAnalyzer:
411
  html = f"""
412
  <div style='border: 2px solid #4CAF50; border-radius: 10px; padding: 20px; margin: 10px 0; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);'>
413
  <h2 style='color: #2E7D32; margin-top: 0; text-align: center;'>🎯 Accent Analysis Results</h2>
414
-
415
  <div style='display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 20px;'>
416
  <div style='flex: 1; min-width: 200px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'>
417
  <h3 style='color: #1976D2; margin-top: 0;'>🎭 Primary Classification</h3>
418
  <p style='font-size: 20px; margin: 5px 0; font-weight: bold;'>{accent_display}</p>
419
  <p style='margin: 5px 0;'>Confidence: <strong style='color: {"#4CAF50" if confidence >= 70 else "#FF9800" if confidence >= 50 else "#F44336"};'>{confidence:.1f}%</strong></p>
420
  </div>
421
-
422
  <div style='flex: 1; min-width: 200px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'>
423
  <h3 style='color: #1976D2; margin-top: 0;'>🌍 English Proficiency</h3>
424
  <p style='font-size: 18px; margin: 5px 0;'><strong style='color: {"#4CAF50" if english_conf >= 70 else "#FF9800" if english_conf >= 50 else "#F44336"};'>{english_conf:.1f}%</strong></p>
425
  <p style='margin: 5px 0;'>Audio Quality: <strong>{quality_score:.0f}/100</strong></p>
426
  </div>
427
-
428
  <div style='flex: 1; min-width: 200px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'>
429
  <h3 style='color: #1976D2; margin-top: 0;'>⏱️ Processing Info</h3>
430
  <p style='margin: 5px 0;'>Duration: <strong>{duration:.1f}s</strong></p>
@@ -432,7 +528,7 @@ class VideoAccentAnalyzer:
432
  <p style='margin: 5px 0;'>Chunks: <strong>{results.get("chunks_analyzed", 1)}</strong></p>
433
  </div>
434
  </div>
435
-
436
  <div style='background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'>
437
  <h3 style='color: #1976D2; margin-top: 0;'>πŸ“Š Assessment</h3>
438
  <div style='display: flex; flex-wrap: wrap; gap: 10px;'>
@@ -477,7 +573,7 @@ class VideoAccentAnalyzer:
477
 
478
  # Create color map
479
  colors = ['#4CAF50' if p == max(probs) else '#2196F3' if p >= 20 else '#FFC107' if p >= 10 else '#9E9E9E'
480
- for p in probs]
481
 
482
  bars = plt.bar(accents, probs, color=colors, alpha=0.8, edgecolor='black', linewidth=0.5)
483
 
@@ -490,8 +586,8 @@ class VideoAccentAnalyzer:
490
  # Add value labels on bars
491
  for bar, prob in zip(bars, probs):
492
  height = bar.get_height()
493
- plt.text(bar.get_x() + bar.get_width() / 2., height + 0.5,
494
- f'{prob:.1f}%', ha='center', va='bottom', fontweight='bold')
495
 
496
  plt.tight_layout()
497
  plt.show()
@@ -507,7 +603,7 @@ class VideoAccentAnalyzer:
507
  print(f"πŸš€ Starting batch analysis of {len(urls)} videos")
508
 
509
  for i, url in enumerate(urls, 1):
510
- print(f"\n{'=' * 60}")
511
  print(f"Processing video {i}/{len(urls)}")
512
 
513
  result = self.analyze_video_url(url, max_duration)
@@ -553,7 +649,6 @@ class VideoAccentAnalyzer:
553
  except Exception as e:
554
  print(f"⚠️ Cleanup warning: {e}")
555
 
556
-
557
  # Helper Functions
558
  def show_examples():
559
  """Show usage examples"""
@@ -574,36 +669,40 @@ def show_examples():
574
  print(" β€’ Multiple speakers may affect accuracy")
575
  print(" β€’ Model works best with sustained speech")
576
 
577
-
578
- def quick_test_local():
579
- """Interactive test for local files"""
580
- print("πŸ” Quick Test Mode for Local Files")
581
- print("πŸ“ Common Kaggle input paths:")
582
- print(" /kaggle/input/your-dataset/video.mp4")
583
- print(" /kaggle/input/video-files/sample.mp4")
584
-
585
- file_path = input("\nπŸ“Ž Enter full path to your local video: ").strip()
586
- if not file_path:
587
- print("❌ No path provided.")
 
588
  return None
589
 
590
- if not os.path.exists(file_path):
591
- print(f"❌ File not found: {file_path}")
592
- return None
 
 
 
593
 
594
  analyzer = VideoAccentAnalyzer()
595
  try:
596
- results = analyzer.analyze_local_video(file_path)
 
597
  analyzer.display_results(results)
598
  return results
599
  finally:
600
  analyzer.cleanup()
601
 
602
-
603
  def demo_analysis():
604
  """Demo function with example usage"""
605
  print("🎬 Video Accent Analyzer Demo")
606
- print("=" * 50)
607
 
608
  # Initialize analyzer
609
  analyzer = VideoAccentAnalyzer()
@@ -623,6 +722,5 @@ def demo_analysis():
623
  print("3. analyzer.display_results(results)")
624
  print("4. analyzer.cleanup() # Clean up temporary files")
625
 
626
-
627
  # Show examples on import
628
  show_examples()
 
1
+ """
2
+ Enhanced Video Accent Analyzer
3
+ Supports YouTube, Loom, direct MP4 links, and local video files with improved error handling and features.
4
+
5
+ """
6
 
7
  import os
8
  import sys
 
22
  # Suppress warnings for cleaner output
23
  warnings.filterwarnings('ignore')
24
 
 
25
  def install_if_missing(packages):
26
  """Install packages if they're not already available in Kaggle"""
27
  for package in packages:
 
34
  print(f"Installing {package}...")
35
  subprocess.check_call([sys.executable, "-m", "pip", "install", package, "--quiet"])
36
 
 
37
  # Required packages for Kaggle
38
  required_packages = [
39
  "yt-dlp",
 
55
  import soundfile as sf
56
  import yt_dlp
57
 
 
58
  class VideoAccentAnalyzer:
59
  def __init__(self, model_name="dima806/multiple_accent_classification"):
60
  """Initialize the accent analyzer for Kaggle environment"""
 
103
 
104
  return True, url
105
 
106
+
107
+ def trim_video(self, input_path, output_path, duration):
108
+ try:
109
+ cmd = ['ffmpeg', '-i', input_path, '-t', str(duration), '-c', 'copy', output_path, '-y']
110
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
111
+ if result.returncode == 0:
112
+ print(f"βœ‚οΈ Trimmed video to {duration} seconds")
113
+ return output_path
114
+ else:
115
+ print(f"❌ Trimming failed: {result.stderr}")
116
+ return input_path # fallback to original
117
+ except Exception as e:
118
+ print(f"❌ Trimming exception: {e}")
119
+ return input_path
120
+
121
  def download_video(self, url, max_duration=None):
122
  """Download video using yt-dlp with improved error handling"""
123
  is_valid, result = self._validate_url(url)
 
128
  url = result
129
  output_path = os.path.join(self.temp_dir, "video.%(ext)s")
130
 
131
+ # Enhanced yt-dlp options for better compatibility
132
  ydl_opts = {
133
  'outtmpl': output_path,
134
+ 'format': 'worst[ext=mp4]/worst', # Use worst quality for faster download and better compatibility
135
+ 'quiet': False, # Show some output for debugging
136
+ 'no_warnings': False,
137
+ 'socket_timeout': 60,
138
+ 'retries': 5,
139
+ 'fragment_retries': 5,
140
+ 'extract_flat': False,
141
+ 'writeinfojson': False,
142
+ 'writethumbnail': False,
143
+ 'writesubtitles': False,
144
  }
145
 
146
  if max_duration:
147
+ # More generous time limit for download
148
+ ydl_opts['match_filter'] = lambda info: None if info.get('duration', 0) <= 200000 else "Video too long"
149
 
150
  try:
151
  with yt_dlp.YoutubeDL(ydl_opts) as ydl:
152
  print(f"πŸ“₯ Downloading video from: {url}")
153
  start_time = time.time()
154
+
155
+ # Get video info first
156
+ try:
157
+ info = ydl.extract_info(url, download=False)
158
+ print(f"πŸ“Ί Found video: {info.get('title', 'Unknown')} ({info.get('duration', 0)}s)")
159
+ except Exception as e:
160
+ print(f"⚠️ Could not extract video info: {e}")
161
+
162
+ # Download the video
163
  ydl.download([url])
164
  download_time = time.time() - start_time
165
 
166
+ # Find downloaded file (try multiple patterns)
167
+ video_path = None
168
  for file in os.listdir(self.temp_dir):
169
+ if file.startswith("video.") and os.path.getsize(os.path.join(self.temp_dir, file)) > 1000: # At least 1KB
170
+ potential_path = os.path.join(self.temp_dir, file)
171
+ print(f"πŸ“ Found downloaded file: {file} ({os.path.getsize(potential_path)/1024:.1f}KB)")
172
+
173
+ # Try basic validation - if ffprobe fails, still try to extract audio
174
+ if self._is_valid_video(potential_path):
175
+ print(f"βœ… Video validation passed: {file}")
176
+ video_path = potential_path
177
+ break
178
  else:
179
+ print(f"⚠️ Video validation failed, but continuing with: {file}")
180
+ video_path = potential_path # Still try to use it
181
+ break
182
+
183
+ if video_path:
184
+ print(f"βœ… Downloaded video: {os.path.basename(video_path)} ({download_time:.1f}s)")
185
+ return video_path
186
+ else:
187
+ print("❌ No video files found after download")
188
+ return None
189
 
190
  except Exception as e:
191
  print(f"⚠️ yt-dlp failed: {e}")
192
  return self._try_direct_download(url)
193
 
194
  def _is_valid_video(self, file_path):
195
+ """Verify video file has valid structure (more lenient)"""
196
  try:
197
+ # First check if file exists and has reasonable size
198
+ if not os.path.exists(file_path) or os.path.getsize(file_path) < 1000:
199
+ return False
200
+
201
+ # Try ffprobe with more lenient settings
202
  result = subprocess.run(
203
+ ['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_format', file_path],
204
+ capture_output=True, text=True, timeout=15
205
+ )
206
+
207
+ if result.returncode == 0:
208
+ try:
209
+ # Try to parse the JSON output
210
+ info = json.loads(result.stdout)
211
+ # Check if we have format information
212
+ if 'format' in info and 'duration' in info['format']:
213
+ return True
214
+ except json.JSONDecodeError:
215
+ pass
216
+
217
+ # If ffprobe fails, try a simpler check - just see if ffmpeg can read it
218
+ result2 = subprocess.run(
219
+ ['ffmpeg', '-i', file_path, '-t', '1', '-f', 'null', '-', '-v', 'quiet'],
220
+ capture_output=True, text=True, timeout=15
221
  )
222
+
223
+ return result2.returncode == 0
224
+
225
  except subprocess.TimeoutExpired:
226
+ print("⚠️ Video validation timed out, assuming valid")
227
+ return True # If validation times out, assume it's valid
228
  except Exception as e:
229
+ print(f"⚠️ Video validation error: {e}, assuming valid")
230
+ return True # If validation fails, assume it's valid and let audio extraction handle it
231
 
232
  def _try_direct_download(self, url):
233
  """Enhanced fallback for direct video URLs"""
 
254
  f.write(chunk)
255
  file_size += len(chunk)
256
 
257
+ print(f"πŸ“ Downloaded {file_size / (1024*1024):.1f} MB")
258
 
259
  if self._is_valid_video(video_path):
260
  print("βœ… Direct download successful")
 
271
  """Extract audio with improved error handling and progress"""
272
  audio_path = os.path.join(self.temp_dir, "audio.wav")
273
 
274
+ # Enhanced ffmpeg command with better error handling
275
  cmd = ['ffmpeg', '-i', video_path, '-vn', '-acodec', 'pcm_s16le',
276
+ '-ar', '16000', '-ac', '1', '-y', '-v', 'warning']
277
 
278
  if max_duration:
279
  cmd.extend(['-t', str(max_duration)])
 
282
  try:
283
  print(f"🎡 Extracting audio (max {max_duration}s)...")
284
  start_time = time.time()
285
+
286
+ # Run ffmpeg with more detailed output for debugging
287
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
288
  extraction_time = time.time() - start_time
289
 
290
+ if result.returncode == 0 and os.path.exists(audio_path) and os.path.getsize(audio_path) > 1000:
291
+ file_size = os.path.getsize(audio_path) / (1024*1024)
292
  print(f"βœ… Audio extracted successfully ({extraction_time:.1f}s, {file_size:.1f}MB)")
293
  return audio_path
294
  else:
295
+ print(f"❌ FFmpeg stderr: {result.stderr}")
296
+ print(f"❌ FFmpeg stdout: {result.stdout}")
297
+
298
+ # Try alternative extraction method
299
+ print("πŸ”„ Trying alternative audio extraction...")
300
+ cmd_alt = ['ffmpeg', '-i', video_path, '-vn', '-acodec', 'libmp3lame',
301
+ '-ar', '16000', '-ac', '1', '-y', '-v', 'warning']
302
+ if max_duration:
303
+ cmd_alt.extend(['-t', str(max_duration)])
304
+
305
+ audio_path_alt = os.path.join(self.temp_dir, "audio.mp3")
306
+ cmd_alt.append(audio_path_alt)
307
+
308
+ result_alt = subprocess.run(cmd_alt, capture_output=True, text=True, timeout=180)
309
+
310
+ if result_alt.returncode == 0 and os.path.exists(audio_path_alt):
311
+ # Convert mp3 to wav
312
+ cmd_convert = ['ffmpeg', '-i', audio_path_alt, '-ar', '16000', '-ac', '1',
313
+ audio_path, '-y', '-v', 'quiet']
314
+ result_convert = subprocess.run(cmd_convert, capture_output=True, text=True, timeout=60)
315
+
316
+ if result_convert.returncode == 0 and os.path.exists(audio_path):
317
+ file_size = os.path.getsize(audio_path) / (1024*1024)
318
+ print(f"βœ… Alternative extraction successful ({file_size:.1f}MB)")
319
+ return audio_path
320
+
321
+ raise Exception(f"Both extraction methods failed. FFmpeg error: {result.stderr}")
322
 
323
  except subprocess.TimeoutExpired:
324
  print("❌ Audio extraction timed out")
 
463
  return {"error": f"File not found: {file_path}"}
464
 
465
  # Check file size
466
+ file_size = os.path.getsize(file_path) / (1024*1024) # MB
467
  print(f"πŸ“ File size: {file_size:.1f} MB")
468
 
469
  video_filename = os.path.basename(file_path)
 
491
  def display_results(self, results):
492
  """Enhanced results display with visualizations"""
493
  if 'error' in results:
494
+ display(HTML(f"<div style='color: red; font-size: 16px; padding: 10px; border: 1px solid red; border-radius: 5px;'>❌ {results['error']}</div>"))
 
495
  return
496
 
497
  accent = results['predicted_accent']
 
507
  html = f"""
508
  <div style='border: 2px solid #4CAF50; border-radius: 10px; padding: 20px; margin: 10px 0; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);'>
509
  <h2 style='color: #2E7D32; margin-top: 0; text-align: center;'>🎯 Accent Analysis Results</h2>
510
+
511
  <div style='display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 20px;'>
512
  <div style='flex: 1; min-width: 200px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'>
513
  <h3 style='color: #1976D2; margin-top: 0;'>🎭 Primary Classification</h3>
514
  <p style='font-size: 20px; margin: 5px 0; font-weight: bold;'>{accent_display}</p>
515
  <p style='margin: 5px 0;'>Confidence: <strong style='color: {"#4CAF50" if confidence >= 70 else "#FF9800" if confidence >= 50 else "#F44336"};'>{confidence:.1f}%</strong></p>
516
  </div>
517
+
518
  <div style='flex: 1; min-width: 200px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'>
519
  <h3 style='color: #1976D2; margin-top: 0;'>🌍 English Proficiency</h3>
520
  <p style='font-size: 18px; margin: 5px 0;'><strong style='color: {"#4CAF50" if english_conf >= 70 else "#FF9800" if english_conf >= 50 else "#F44336"};'>{english_conf:.1f}%</strong></p>
521
  <p style='margin: 5px 0;'>Audio Quality: <strong>{quality_score:.0f}/100</strong></p>
522
  </div>
523
+
524
  <div style='flex: 1; min-width: 200px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'>
525
  <h3 style='color: #1976D2; margin-top: 0;'>⏱️ Processing Info</h3>
526
  <p style='margin: 5px 0;'>Duration: <strong>{duration:.1f}s</strong></p>
 
528
  <p style='margin: 5px 0;'>Chunks: <strong>{results.get("chunks_analyzed", 1)}</strong></p>
529
  </div>
530
  </div>
531
+
532
  <div style='background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'>
533
  <h3 style='color: #1976D2; margin-top: 0;'>πŸ“Š Assessment</h3>
534
  <div style='display: flex; flex-wrap: wrap; gap: 10px;'>
 
573
 
574
  # Create color map
575
  colors = ['#4CAF50' if p == max(probs) else '#2196F3' if p >= 20 else '#FFC107' if p >= 10 else '#9E9E9E'
576
+ for p in probs]
577
 
578
  bars = plt.bar(accents, probs, color=colors, alpha=0.8, edgecolor='black', linewidth=0.5)
579
 
 
586
  # Add value labels on bars
587
  for bar, prob in zip(bars, probs):
588
  height = bar.get_height()
589
+ plt.text(bar.get_x() + bar.get_width()/2., height + 0.5,
590
+ f'{prob:.1f}%', ha='center', va='bottom', fontweight='bold')
591
 
592
  plt.tight_layout()
593
  plt.show()
 
603
  print(f"πŸš€ Starting batch analysis of {len(urls)} videos")
604
 
605
  for i, url in enumerate(urls, 1):
606
+ print(f"\n{'='*60}")
607
  print(f"Processing video {i}/{len(urls)}")
608
 
609
  result = self.analyze_video_url(url, max_duration)
 
649
  except Exception as e:
650
  print(f"⚠️ Cleanup warning: {e}")
651
 
 
652
  # Helper Functions
653
  def show_examples():
654
  """Show usage examples"""
 
669
  print(" β€’ Multiple speakers may affect accuracy")
670
  print(" β€’ Model works best with sustained speech")
671
 
672
+ def quick_test_url():
673
+ """Interactive test for video URLs"""
674
+ print("πŸ” Quick Test Mode for Video URLs")
675
+ print("🎯 Supported: YouTube, Loom, Direct MP4 links")
676
+ print("πŸ’‘ Examples:")
677
+ print(" YouTube: https://youtube.com/watch?v=VIDEO_ID")
678
+ print(" Loom: https://www.loom.com/share/VIDEO_ID")
679
+ print(" Direct: https://example.com/video.mp4")
680
+
681
+ url = input("\nπŸ“Ž Enter your video URL (Loom, YouTube, etc.): ").strip()
682
+ if not url:
683
+ print("❌ No URL provided.")
684
  return None
685
 
686
+ max_duration = input("⏱️ Max duration in seconds (default 20): ").strip()
687
+ try:
688
+ max_duration = int(max_duration) if max_duration else 20
689
+ except ValueError:
690
+ max_duration = 20
691
+ print(f"⚠️ Invalid duration, using {max_duration} seconds")
692
 
693
  analyzer = VideoAccentAnalyzer()
694
  try:
695
+ print(f"\nπŸš€ Starting analysis...")
696
+ results = analyzer.analyze_video_url(url, max_duration=max_duration)
697
  analyzer.display_results(results)
698
  return results
699
  finally:
700
  analyzer.cleanup()
701
 
 
702
  def demo_analysis():
703
  """Demo function with example usage"""
704
  print("🎬 Video Accent Analyzer Demo")
705
+ print("="*50)
706
 
707
  # Initialize analyzer
708
  analyzer = VideoAccentAnalyzer()
 
722
  print("3. analyzer.display_results(results)")
723
  print("4. analyzer.cleanup() # Clean up temporary files")
724
 
 
725
  # Show examples on import
726
  show_examples()