Spaces:
Paused
Paused
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 +2 -1
- app/models/swing_analyzer.py +29 -185
- app/utils/video_downloader.py +343 -76
- app/utils/visualizer.py +5 -7
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 |
-
|
| 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 |
-
|
| 27 |
-
|
| 28 |
-
|
| 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
|
| 105 |
"""
|
| 106 |
-
Simple
|
| 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 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 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 |
-
|
| 169 |
-
|
| 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 |
-
"
|
| 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 |
-
#
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
'
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 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 |
-
#
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
video_path = os.path.join(output_dir, mp4_files[0])
|
| 153 |
-
else:
|
| 154 |
-
raise ValueError("Downloaded file not found")
|
| 155 |
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 185 |
-
|
| 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 |
-
|
| 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"]:
|