chenemii commited on
Commit
34aaec8
·
1 Parent(s): 411d7d0

Refactor and improve core application modules

Browse files

- main.py: Add fps parameter to segment_swing call for compatibility
- models/swing_analyzer.py: Major refactoring to use new segmentation module
* Removed 156 lines of complex legacy code
* Added clean wrapper functions for backward compatibility
* Simplified trajectory analysis
- utils/video_downloader.py: Enhanced video downloading functionality (+348 lines)
- utils/visualizer.py: Minor improvements to video annotation

These modules are actively used by both the CLI (main.py) and web interface (streamlit_app.py)

app/main.py CHANGED
@@ -57,7 +57,8 @@ def main():
57
  print("\nSegmenting swing phases...")
58
  swing_phases = segment_swing(pose_data,
59
  detections,
60
- sample_rate=sample_rate)
 
61
 
62
  # Step 7: Analyze trajectory and speed
63
  print("\nAnalyzing trajectory and speed...")
 
57
  print("\nSegmenting swing phases...")
58
  swing_phases = segment_swing(pose_data,
59
  detections,
60
+ sample_rate=sample_rate,
61
+ fps=30.0)
62
 
63
  # Step 7: Analyze trajectory and speed
64
  print("\nAnalyzing trajectory and speed...")
app/models/swing_analyzer.py CHANGED
@@ -4,205 +4,49 @@ Swing analysis module for golf swing segmentation and trajectory analysis
4
 
5
  import numpy as np
6
  from .pose_estimator import calculate_joint_angles
 
7
 
 
 
8
 
9
- def find_top_of_backswing(pose_data):
10
- """Helper function to find the peak of backswing"""
11
- frame_indices = sorted(pose_data.keys())
12
- max_shoulder_angle = -1
13
- top_frame = frame_indices[0]
14
-
15
- for idx in frame_indices:
16
- keypoints = pose_data[idx]
17
- angles = calculate_joint_angles(keypoints)
18
- shoulder = angles.get("right_shoulder", 0)
19
- if shoulder > max_shoulder_angle:
20
- max_shoulder_angle = shoulder
21
- top_frame = idx
22
-
23
- return top_frame
24
 
25
 
26
- def detect_impact_frame(pose_data, detections, sample_rate=1):
27
- """
28
- Simple impact detection: ball movement first, wrist speed fallback
29
- """
30
- frame_indices = sorted(pose_data.keys())
31
- if len(frame_indices) < 10:
32
- return None
33
-
34
- top_backswing = find_top_of_backswing(pose_data)
35
- downswing_frames = [f for f in frame_indices if f > top_backswing]
36
-
37
- if not downswing_frames:
38
- return None
39
-
40
- # Method 1: Ball movement (if we have ball detections)
41
- if detections:
42
- ball_detections = [d for d in detections if d.class_name == "sports ball"]
43
- ball_positions = {}
44
-
45
- # Create a mapping from original video frame indices to processed frame indices
46
- original_to_processed = {}
47
- for processed_idx in frame_indices:
48
- original_frame_idx = processed_idx * sample_rate
49
- original_to_processed[original_frame_idx] = processed_idx
50
-
51
- for detection in ball_detections:
52
- original_frame_idx = detection.frame_idx
53
- # Find the closest processed frame index
54
- processed_frame_idx = None
55
- if original_frame_idx in original_to_processed:
56
- processed_frame_idx = original_to_processed[original_frame_idx]
57
- else:
58
- # Find closest processed frame
59
- closest_original = min(original_to_processed.keys(),
60
- key=lambda x: abs(x - original_frame_idx))
61
- if abs(closest_original - original_frame_idx) <= sample_rate:
62
- processed_frame_idx = original_to_processed[closest_original]
63
-
64
- if processed_frame_idx and processed_frame_idx > top_backswing:
65
- x1, y1, x2, y2 = detection.bbox
66
- center_x, center_y = (x1 + x2) / 2, (y1 + y2) / 2
67
- ball_positions[processed_frame_idx] = (center_x, center_y)
68
-
69
- # Find first significant ball movement
70
- if len(ball_positions) >= 2:
71
- sorted_frames = sorted(ball_positions.keys())
72
- for i in range(1, len(sorted_frames)):
73
- curr_pos = ball_positions[sorted_frames[i]]
74
- prev_pos = ball_positions[sorted_frames[i-1]]
75
- movement = np.sqrt((curr_pos[0] - prev_pos[0])**2 + (curr_pos[1] - prev_pos[1])**2)
76
-
77
- if movement > 15: # Significant movement threshold
78
- print(f"Impact detected via ball movement at processed frame {sorted_frames[i]} (original frame {sorted_frames[i] * sample_rate})")
79
- return sorted_frames[i]
80
-
81
- # Method 2: Wrist speed fallback (simple and reliable)
82
- max_wrist_speed = 0
83
- impact_frame = None
84
-
85
- for i in range(1, len(downswing_frames)):
86
- curr_frame = downswing_frames[i]
87
- prev_frame = downswing_frames[i-1]
88
-
89
- curr_angles = calculate_joint_angles(pose_data[curr_frame])
90
- prev_angles = calculate_joint_angles(pose_data[prev_frame])
91
-
92
- curr_wrist = curr_angles.get("right_wrist", 0)
93
- prev_wrist = prev_angles.get("right_wrist", 0)
94
- wrist_speed = abs(curr_wrist - prev_wrist)
95
-
96
- if wrist_speed > max_wrist_speed:
97
- max_wrist_speed = wrist_speed
98
- impact_frame = curr_frame
99
-
100
- print(f"Impact detected via wrist speed at processed frame {impact_frame} (original frame {impact_frame * sample_rate if impact_frame else 'N/A'})")
101
- return impact_frame or downswing_frames[len(downswing_frames) // 3]
102
 
103
 
104
- def segment_swing_pose_based(pose_data, detections=None, sample_rate=1):
105
  """
106
- Simple swing segmentation with clean impact detection
107
- """
108
- swing_phases = {"setup": [], "backswing": [], "downswing": [], "impact": [], "follow_through": []}
109
- frame_indices = sorted(pose_data.keys())
110
-
111
- if not frame_indices:
112
- return swing_phases
113
-
114
- # 1. Find setup end (first significant movement)
115
- setup_end = frame_indices[0]
116
- initial_angles = calculate_joint_angles(pose_data[frame_indices[0]])
117
- initial_shoulder = initial_angles.get("right_shoulder", 0)
118
 
119
- for idx in frame_indices[1:]:
120
- angles = calculate_joint_angles(pose_data[idx])
121
- shoulder = angles.get("right_shoulder", 0)
122
- if abs(shoulder - initial_shoulder) > 10:
123
- setup_end = max(frame_indices[0], idx - 2)
124
- break
125
-
126
- # 2. Find top of backswing
127
- top_backswing = find_top_of_backswing(pose_data)
128
-
129
- # 3. Find impact frame
130
- impact_frame = detect_impact_frame(pose_data, detections, sample_rate)
131
-
132
- # Simple validation and fallback
133
- if not impact_frame or impact_frame <= top_backswing:
134
- downswing_frames = [f for f in frame_indices if f > top_backswing]
135
- impact_frame = downswing_frames[len(downswing_frames) // 3] if downswing_frames else top_backswing + 1
136
-
137
- print(f"Swing phases: Setup end={setup_end} (orig {setup_end * sample_rate}), Top backswing={top_backswing} (orig {top_backswing * sample_rate}), Impact={impact_frame} (orig {impact_frame * sample_rate if impact_frame else 'N/A'})")
138
-
139
- # 4. Assign phases
140
- for idx in frame_indices:
141
- if idx <= setup_end:
142
- swing_phases["setup"].append(idx)
143
- elif idx <= top_backswing:
144
- swing_phases["backswing"].append(idx)
145
- elif idx < impact_frame:
146
- swing_phases["downswing"].append(idx)
147
- elif idx == impact_frame:
148
- swing_phases["impact"].append(idx)
149
- else:
150
- swing_phases["follow_through"].append(idx)
151
-
152
- return swing_phases
153
-
154
-
155
- # Wrapper function to maintain compatibility with existing Streamlit app
156
- def segment_swing(pose_data, detections, sample_rate=1):
157
- """
158
- Main swing segmentation function (wrapper for pose-based approach)
159
- """
160
- return segment_swing_pose_based(pose_data, detections, sample_rate)
161
-
162
-
163
- def analyze_trajectory(frames, detections, swing_phases, sample_rate=1):
164
- """
165
- Analyze ball trajectory and calculate club speed
166
  """
167
  trajectory_data = {}
168
- if len(frames) < 150:
169
- sample_rate = 1
170
-
171
- ball_detections = [d for d in detections if d.class_name == "sports ball"]
172
- impact_frames = swing_phases.get("impact", [])
173
- if not impact_frames:
174
- return trajectory_data
175
-
176
- impact_frame_idx = impact_frames[len(impact_frames) // 2]
177
- ball_trajectory = []
178
- ball_positions = {}
179
-
180
- for detection in ball_detections:
181
- frame_idx = detection.frame_idx // sample_rate
182
- if frame_idx >= impact_frame_idx:
183
- x1, y1, x2, y2 = detection.bbox
184
- center_x = (x1 + x2) / 2
185
- center_y = (y1 + y2) / 2
186
- ball_positions[frame_idx] = (center_x, center_y)
187
-
188
- sorted_frames = sorted(ball_positions.keys())
189
- for idx in sorted_frames:
190
- ball_trajectory.append(ball_positions[idx])
191
-
192
- club_speed = None
193
- downswing_frames = swing_phases.get("downswing", [])
194
- if len(downswing_frames) >= 2:
195
- actual_frames_elapsed = (downswing_frames[-1] - downswing_frames[0]) * sample_rate
196
- time_diff = actual_frames_elapsed / 30
197
- if time_diff > 0:
198
- club_speed = 100 * (1 / time_diff)
199
-
200
  for phase_name, frames_in_phase in swing_phases.items():
 
 
 
201
  for frame_idx in frames_in_phase:
202
  trajectory_data[frame_idx] = {
203
  "phase": phase_name,
204
- "club_speed": club_speed if phase_name == "impact" else None,
205
- "ball_trajectory": ball_trajectory if phase_name in ["impact", "follow_through"] else None
206
  }
207
 
 
 
 
 
 
 
 
208
  return trajectory_data
 
4
 
5
  import numpy as np
6
  from .pose_estimator import calculate_joint_angles
7
+ from .segmentation import segment_swing
8
 
9
+ # One-liner frame mapping replacement
10
+ def to_processed_idx(original_idx, sample_rate): return int(round(original_idx / max(1, sample_rate)))
11
 
12
+ # Legacy functions replaced by segmentation.py - kept for compatibility if needed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
 
15
+ # Legacy wrapper - now redirects to new segmentation module
16
+ def segment_swing_pose_based(pose_data, detections=None, sample_rate=1, frame_shape=None, **kwargs):
17
+ """Legacy function - use segmentation.segment_swing directly"""
18
+ return segment_swing(pose_data, detections, sample_rate, frame_shape, **kwargs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
 
21
+ def analyze_trajectory(frames, detections, swing_phases, sample_rate=1, fps=30.0):
22
  """
23
+ Simple trajectory analysis - just track ball movement after impact
 
 
 
 
 
 
 
 
 
 
 
24
 
25
+ Args:
26
+ frames: Video frames
27
+ detections: Ball detections
28
+ swing_phases: Swing phase data
29
+ sample_rate: Frame sampling rate
30
+ fps: Actual video FPS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  """
32
  trajectory_data = {}
33
+
34
+ # Simple phase assignment without complex calculations
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  for phase_name, frames_in_phase in swing_phases.items():
36
+ # Skip non-phase keys like timing_unreliable
37
+ if not isinstance(frames_in_phase, list):
38
+ continue
39
  for frame_idx in frames_in_phase:
40
  trajectory_data[frame_idx] = {
41
  "phase": phase_name,
42
+ "ball_detected": False
 
43
  }
44
 
45
+ # Mark frames where ball is detected
46
+ ball_detections = [d for d in detections if d.class_name == "sports ball"]
47
+ for detection in ball_detections:
48
+ frame_idx = to_processed_idx(detection.frame_idx, sample_rate)
49
+ if frame_idx in trajectory_data:
50
+ trajectory_data[frame_idx]["ball_detected"] = True
51
+
52
  return trajectory_data
app/utils/video_downloader.py CHANGED
@@ -3,6 +3,8 @@ YouTube video downloader module using yt-dlp
3
  """
4
 
5
  import os
 
 
6
  import yt_dlp
7
 
8
 
@@ -88,9 +90,188 @@ def cleanup_downloads_directory(output_dir="downloads", keep_annotated=True):
88
  return {"error": str(e)}
89
 
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  def download_youtube_video(url, output_dir="downloads"):
92
  """
93
- Download a YouTube video from the provided URL using yt-dlp
94
 
95
  Args:
96
  url (str): YouTube video URL
@@ -108,62 +289,127 @@ def download_youtube_video(url, output_dir="downloads"):
108
  # Set output template for the downloaded file
109
  output_template = os.path.join(output_dir, "%(title)s.%(ext)s")
110
 
111
- # Configure yt-dlp options
112
- ydl_opts = {
113
- 'format': 'best[ext=mp4]/best', # Prefer mp4 format
114
- 'outtmpl': output_template,
115
- 'noplaylist': True,
116
- 'quiet': False,
117
- 'no_warnings': False,
118
- 'ignoreerrors': False,
119
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
- try:
122
- # Create yt-dlp object and download the video
123
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
124
- info = ydl.extract_info(url, download=True)
125
-
126
- # Get the downloaded file path
127
- if 'entries' in info:
128
- # Playlist (should not happen with noplaylist=True)
129
- raise ValueError("Playlists are not supported")
130
-
131
- # Get video title and extension
132
- title = info.get('title', 'video')
133
- ext = info.get('ext', 'mp4')
134
-
135
- # Construct the file path
136
- video_path = os.path.join(output_dir, f"{title}.{ext}")
137
-
138
- # Check if file exists
139
- if not os.path.exists(video_path):
140
- # Try with sanitized filename
141
- sanitized_title = ''.join(c for c in title
142
- if c.isalnum() or c in ' ._-')
143
- video_path = os.path.join(output_dir,
144
- f"{sanitized_title}.{ext}")
145
 
 
146
  if not os.path.exists(video_path):
147
- # If still not found, look for any mp4 file in the directory
148
- mp4_files = [
149
- f for f in os.listdir(output_dir) if f.endswith('.mp4')
150
- ]
151
- if mp4_files:
152
- video_path = os.path.join(output_dir, mp4_files[0])
153
- else:
154
- raise ValueError("Downloaded file not found")
155
 
156
- return video_path
 
 
 
 
 
 
 
 
157
 
158
- except yt_dlp.utils.DownloadError as e:
159
- raise ValueError(f"Error downloading video: {str(e)}")
160
- except Exception as e:
161
- raise ValueError(f"Error: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
 
164
  def download_pro_reference(url="https://www.youtube.com/shorts/geR666LWSHg", output_dir="downloads"):
165
  """
166
- Download a professional golfer reference video
167
 
168
  Args:
169
  url (str): YouTube video URL of professional golfer (default: provided reference)
@@ -179,41 +425,62 @@ def download_pro_reference(url="https://www.youtube.com/shorts/geR666LWSHg", out
179
  # Check if pro reference already exists to avoid re-downloading
180
  pro_file_path = os.path.join(output_dir, "pro_reference.mp4")
181
  if os.path.exists(pro_file_path):
 
182
  return pro_file_path
183
 
184
- # Set output template for the downloaded file with fixed name
185
- output_template = os.path.join(output_dir, "pro_reference.%(ext)s")
186
-
187
- # Configure yt-dlp options
188
- ydl_opts = {
189
- 'format': 'best[ext=mp4]/best', # Prefer mp4 format
190
- 'outtmpl': output_template,
191
- 'noplaylist': True,
192
- 'quiet': False,
193
- 'no_warnings': False,
194
- 'ignoreerrors': False,
195
- }
196
-
197
- # Create yt-dlp object and download the video
198
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
199
- ydl.extract_info(url, download=True)
200
-
201
- # Check if file exists with mp4 extension
202
- if os.path.exists(pro_file_path):
203
- return pro_file_path
204
- else:
205
- # Try other extensions
206
- for ext in ['webm', 'mkv']:
207
- alt_path = os.path.join(output_dir, f"pro_reference.{ext}")
208
- if os.path.exists(alt_path):
209
- return alt_path
210
-
211
- # If still not found, download as normal video and rename
212
  video_path = download_youtube_video(url, output_dir)
 
 
213
  ext = os.path.splitext(video_path)[1]
214
  new_path = os.path.join(output_dir, f"pro_reference{ext}")
215
  os.rename(video_path, new_path)
 
216
  return new_path
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  except Exception as e:
219
  raise ValueError(f"Error downloading pro reference: {str(e)}")
 
3
  """
4
 
5
  import os
6
+ import random
7
+ import subprocess
8
  import yt_dlp
9
 
10
 
 
90
  return {"error": str(e)}
91
 
92
 
93
+ def get_user_agents():
94
+ """Get a list of common user agents to rotate between"""
95
+ return [
96
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
97
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
98
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
99
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0',
100
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:120.0) Gecko/20100101 Firefox/120.0',
101
+ ]
102
+
103
+
104
+ def try_extract_browser_cookies():
105
+ """
106
+ Try to extract cookies from browser automatically
107
+ Returns path to extracted cookies file if successful, None otherwise
108
+ """
109
+ try:
110
+ # Try to extract cookies from Chrome first
111
+ browsers = ['chrome', 'firefox', 'safari', 'edge']
112
+
113
+ for browser in browsers:
114
+ try:
115
+ cookies_path = os.path.expanduser(f"~/.config/yt-dlp/cookies_{browser}.txt")
116
+
117
+ # Use yt-dlp to extract cookies
118
+ cmd = ['yt-dlp', '--cookies-from-browser', browser, '--print-to-file', 'cookies', cookies_path, '--no-download', 'https://www.youtube.com/']
119
+
120
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
121
+
122
+ if result.returncode == 0 and os.path.exists(cookies_path):
123
+ print(f"Successfully extracted cookies from {browser}")
124
+ return cookies_path
125
+
126
+ except (subprocess.TimeoutExpired, FileNotFoundError, Exception):
127
+ continue
128
+
129
+ except Exception:
130
+ pass
131
+
132
+ return None
133
+
134
+
135
+ def find_cookies_file():
136
+ """
137
+ Look for browser cookies file that can be used for YouTube authentication
138
+ Returns the path to cookies file if found, None otherwise
139
+ """
140
+ possible_paths = [
141
+ os.path.expanduser("~/.config/yt-dlp/cookies.txt"),
142
+ os.path.expanduser("~/cookies.txt"),
143
+ "cookies.txt",
144
+ os.path.join(os.getcwd(), "cookies.txt"),
145
+ ]
146
+
147
+ # First check for existing cookies files
148
+ for path in possible_paths:
149
+ if os.path.exists(path):
150
+ print(f"Found existing cookies file: {path}")
151
+ return path
152
+
153
+ # If no existing cookies found, try to extract from browser
154
+ print("No existing cookies found, trying to extract from browser...")
155
+ extracted_cookies = try_extract_browser_cookies()
156
+ if extracted_cookies:
157
+ return extracted_cookies
158
+
159
+ return None
160
+
161
+
162
+ def print_cookie_help():
163
+ """
164
+ Print helpful instructions for setting up cookies to bypass YouTube bot detection
165
+ """
166
+ help_text = """
167
+ 🔧 YouTube Bot Detection Fix - Cookie Setup Instructions:
168
+
169
+ Method 1 - Automatic (Recommended):
170
+ The system will try to automatically extract cookies from your browser.
171
+
172
+ Method 2 - Manual Cookie Export:
173
+ 1. Install a browser extension like "Get cookies.txt LOCALLY"
174
+ 2. Go to youtube.com and make sure you're logged in
175
+ 3. Use the extension to export cookies as 'cookies.txt'
176
+ 4. Save the file in one of these locations:
177
+ • ~/cookies.txt (your home directory)
178
+ • ~/.config/yt-dlp/cookies.txt
179
+ • In the same folder as this script
180
+
181
+ Method 3 - Command Line (Advanced):
182
+ Run: yt-dlp --cookies-from-browser chrome --print-to-file cookies ~/cookies.txt --no-download https://youtube.com
183
+ (Replace 'chrome' with your browser: firefox, safari, edge)
184
+
185
+ Method 4 - Alternative Video Sources:
186
+ • Try using a different YouTube video URL
187
+ • Consider using videos that don't require authentication
188
+
189
+ Note: YouTube's bot detection is sometimes temporary - you can also try again later.
190
+ """
191
+ print(help_text)
192
+
193
+
194
+ def get_fallback_configs():
195
+ """
196
+ Get multiple configuration strategies to try in sequence
197
+ """
198
+ user_agents = get_user_agents()
199
+ cookies_file = find_cookies_file()
200
+
201
+ configs = []
202
+
203
+ # Strategy 1: Use cookies if available
204
+ if cookies_file:
205
+ configs.append({
206
+ 'name': 'with_cookies',
207
+ 'opts': {
208
+ 'cookiefile': cookies_file,
209
+ 'http_headers': {
210
+ 'User-Agent': random.choice(user_agents),
211
+ },
212
+ 'extractor_args': {
213
+ 'youtube': {
214
+ 'player_client': ['android', 'web'],
215
+ }
216
+ },
217
+ }
218
+ })
219
+
220
+ # Strategy 2: Android client (often works better)
221
+ configs.append({
222
+ 'name': 'android_client',
223
+ 'opts': {
224
+ 'http_headers': {
225
+ 'User-Agent': 'com.google.android.youtube/19.09.37 (Linux; U; Android 11) gzip',
226
+ },
227
+ 'extractor_args': {
228
+ 'youtube': {
229
+ 'player_client': ['android'],
230
+ }
231
+ },
232
+ }
233
+ })
234
+
235
+ # Strategy 3: Web client with full headers
236
+ configs.append({
237
+ 'name': 'web_client_full',
238
+ 'opts': {
239
+ 'http_headers': {
240
+ 'User-Agent': random.choice(user_agents),
241
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
242
+ 'Accept-Language': 'en-us,en;q=0.5',
243
+ 'Accept-Encoding': 'gzip,deflate',
244
+ 'Connection': 'keep-alive',
245
+ 'Upgrade-Insecure-Requests': '1',
246
+ 'Sec-Fetch-Dest': 'document',
247
+ 'Sec-Fetch-Mode': 'navigate',
248
+ 'Sec-Fetch-Site': 'none',
249
+ 'Sec-Fetch-User': '?1',
250
+ },
251
+ 'extractor_args': {
252
+ 'youtube': {
253
+ 'player_client': ['web'],
254
+ }
255
+ },
256
+ }
257
+ })
258
+
259
+ # Strategy 4: Basic configuration (fallback)
260
+ configs.append({
261
+ 'name': 'basic',
262
+ 'opts': {
263
+ 'http_headers': {
264
+ 'User-Agent': random.choice(user_agents),
265
+ },
266
+ }
267
+ })
268
+
269
+ return configs
270
+
271
+
272
  def download_youtube_video(url, output_dir="downloads"):
273
  """
274
+ Download a YouTube video from the provided URL using yt-dlp with fallback strategies
275
 
276
  Args:
277
  url (str): YouTube video URL
 
289
  # Set output template for the downloaded file
290
  output_template = os.path.join(output_dir, "%(title)s.%(ext)s")
291
 
292
+ # Get fallback configurations to try
293
+ fallback_configs = get_fallback_configs()
294
+
295
+ last_error = None
296
+
297
+ # Try each configuration strategy
298
+ for config in fallback_configs:
299
+ print(f"Trying download strategy: {config['name']}")
300
+
301
+ # Base yt-dlp options
302
+ ydl_opts = {
303
+ 'format': 'best[ext=mp4]/best', # Prefer mp4 format
304
+ 'outtmpl': output_template,
305
+ 'noplaylist': True,
306
+ 'quiet': False,
307
+ 'no_warnings': False,
308
+ 'ignoreerrors': False,
309
+ 'sleep_interval': 1,
310
+ 'max_sleep_interval': 5,
311
+ }
312
+
313
+ # Merge strategy-specific options
314
+ ydl_opts.update(config['opts'])
315
+
316
+ try:
317
+ # Create yt-dlp object and download the video
318
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
319
+ info = ydl.extract_info(url, download=True)
320
+
321
+ # If we get here, download was successful
322
+ print(f"Download successful with strategy: {config['name']}")
323
+
324
+ # Get the downloaded file path
325
+ if 'entries' in info:
326
+ # Playlist (should not happen with noplaylist=True)
327
+ raise ValueError("Playlists are not supported")
328
 
329
+ # Get video title and extension
330
+ title = info.get('title', 'video')
331
+ ext = info.get('ext', 'mp4')
332
+
333
+ # Construct the file path
334
+ video_path = os.path.join(output_dir, f"{title}.{ext}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
+ # Check if file exists
337
  if not os.path.exists(video_path):
338
+ # Try with sanitized filename
339
+ sanitized_title = ''.join(c for c in title
340
+ if c.isalnum() or c in ' ._-')
341
+ video_path = os.path.join(output_dir,
342
+ f"{sanitized_title}.{ext}")
 
 
 
343
 
344
+ if not os.path.exists(video_path):
345
+ # If still not found, look for any mp4 file in the directory
346
+ mp4_files = [
347
+ f for f in os.listdir(output_dir) if f.endswith('.mp4')
348
+ ]
349
+ if mp4_files:
350
+ video_path = os.path.join(output_dir, mp4_files[0])
351
+ else:
352
+ raise ValueError("Downloaded file not found")
353
 
354
+ return video_path
355
+
356
+ except yt_dlp.utils.DownloadError as e:
357
+ last_error = str(e)
358
+ print(f"Strategy '{config['name']}' failed: {last_error}")
359
+ if "Sign in to confirm you're not a bot" in last_error:
360
+ print("Bot detection encountered, trying next strategy...")
361
+ continue
362
+ elif config == fallback_configs[-1]: # Last strategy failed
363
+ break
364
+ else:
365
+ continue
366
+ except Exception as e:
367
+ last_error = str(e)
368
+ print(f"Strategy '{config['name']}' failed with error: {last_error}")
369
+ continue
370
+
371
+ # If all strategies failed, provide helpful error message
372
+ error_msg = f"All download strategies failed. Last error: {last_error}"
373
+
374
+ if "Sign in to confirm you're not a bot" in (last_error or ""):
375
+ print_cookie_help()
376
+ error_msg += "\n\n⚠️ YouTube bot detection encountered. See the instructions above to fix this issue."
377
+
378
+ raise ValueError(error_msg)
379
+
380
+
381
+ def download_youtube_video_simple(url, output_dir="downloads"):
382
+ """
383
+ Simplified YouTube video downloader - tries the most reliable methods first
384
+
385
+ Args:
386
+ url (str): YouTube video URL
387
+ output_dir (str): Directory to save the downloaded video
388
+
389
+ Returns:
390
+ str: Path to the downloaded video file
391
+
392
+ Raises:
393
+ ValueError: If the URL is invalid or video is unavailable
394
+ """
395
+ print(f"📥 Starting download from: {url}")
396
+
397
+ try:
398
+ return download_youtube_video(url, output_dir)
399
+ except ValueError as e:
400
+ if "Sign in to confirm you're not a bot" in str(e):
401
+ print("\n🤖 YouTube bot detection encountered!")
402
+ print("💡 Quick fixes to try:")
403
+ print(" • Wait a few minutes and try again")
404
+ print(" • Try a different YouTube video")
405
+ print(" • Use a different network/VPN")
406
+ print("\n📋 For persistent issues, run print_cookie_help() for detailed setup instructions")
407
+ raise e
408
 
409
 
410
  def download_pro_reference(url="https://www.youtube.com/shorts/geR666LWSHg", output_dir="downloads"):
411
  """
412
+ Download a professional golfer reference video using improved download methods
413
 
414
  Args:
415
  url (str): YouTube video URL of professional golfer (default: provided reference)
 
425
  # Check if pro reference already exists to avoid re-downloading
426
  pro_file_path = os.path.join(output_dir, "pro_reference.mp4")
427
  if os.path.exists(pro_file_path):
428
+ print("Pro reference video already exists, using cached version")
429
  return pro_file_path
430
 
431
+ # Try to download using the improved download function first
432
+ try:
433
+ print("Downloading pro reference video...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  video_path = download_youtube_video(url, output_dir)
435
+
436
+ # Rename to pro_reference
437
  ext = os.path.splitext(video_path)[1]
438
  new_path = os.path.join(output_dir, f"pro_reference{ext}")
439
  os.rename(video_path, new_path)
440
+ print(f"Pro reference downloaded and saved as: {new_path}")
441
  return new_path
442
 
443
+ except Exception as download_error:
444
+ print(f"Standard download failed: {download_error}")
445
+ print("Trying direct download with fixed name...")
446
+
447
+ # Fallback: try direct download with fixed filename
448
+ output_template = os.path.join(output_dir, "pro_reference.%(ext)s")
449
+ fallback_configs = get_fallback_configs()
450
+
451
+ for config in fallback_configs:
452
+ print(f"Trying pro reference download with strategy: {config['name']}")
453
+
454
+ ydl_opts = {
455
+ 'format': 'best[ext=mp4]/best',
456
+ 'outtmpl': output_template,
457
+ 'noplaylist': True,
458
+ 'quiet': False,
459
+ 'no_warnings': False,
460
+ 'ignoreerrors': False,
461
+ }
462
+ ydl_opts.update(config['opts'])
463
+
464
+ try:
465
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
466
+ ydl.extract_info(url, download=True)
467
+
468
+ # Check if file exists with mp4 extension
469
+ if os.path.exists(pro_file_path):
470
+ print(f"Pro reference downloaded successfully with strategy: {config['name']}")
471
+ return pro_file_path
472
+ else:
473
+ # Try other extensions
474
+ for ext in ['webm', 'mkv']:
475
+ alt_path = os.path.join(output_dir, f"pro_reference.{ext}")
476
+ if os.path.exists(alt_path):
477
+ print(f"Pro reference downloaded as {ext} format")
478
+ return alt_path
479
+ except Exception as e:
480
+ print(f"Pro reference strategy '{config['name']}' failed: {str(e)}")
481
+ continue
482
+
483
+ raise ValueError("All pro reference download strategies failed")
484
+
485
  except Exception as e:
486
  raise ValueError(f"Error downloading pro reference: {str(e)}")
app/utils/visualizer.py CHANGED
@@ -295,6 +295,9 @@ def create_annotated_video(video_path,
295
  # Draw swing phase information
296
  phase = None
297
  for phase_name, phase_frames in swing_phases.items():
 
 
 
298
  if i in phase_frames:
299
  phase = phase_name
300
  break
@@ -303,15 +306,10 @@ def create_annotated_video(video_path,
303
  cv2.putText(annotated_frame, f"Phase: {phase}", (10, 30),
304
  cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
305
 
306
- # Draw trajectory information if available
307
  if i in trajectory_data:
308
  traj_info = trajectory_data[i]
309
- if "club_speed" in traj_info and traj_info["club_speed"]:
310
- cv2.putText(
311
- annotated_frame,
312
- f"Club Speed: {traj_info['club_speed']:.1f} mph",
313
- (10, 70), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0),
314
- 2)
315
 
316
  # Adjust ball trajectory points if we rotated the frame
317
  if "ball_trajectory" in traj_info and traj_info["ball_trajectory"]:
 
295
  # Draw swing phase information
296
  phase = None
297
  for phase_name, phase_frames in swing_phases.items():
298
+ # Skip non-phase keys like timing_unreliable
299
+ if not isinstance(phase_frames, list):
300
+ continue
301
  if i in phase_frames:
302
  phase = phase_name
303
  break
 
306
  cv2.putText(annotated_frame, f"Phase: {phase}", (10, 30),
307
  cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
308
 
309
+ # Draw trajectory information if available
310
  if i in trajectory_data:
311
  traj_info = trajectory_data[i]
312
+ # Club speed display removed - not part of 5 core metrics
 
 
 
 
 
313
 
314
  # Adjust ball trajectory points if we rotated the frame
315
  if "ball_trajectory" in traj_info and traj_info["ball_trajectory"]: