atheletic-performance-analysis / athletic_performance.py
akarsh999's picture
Upload 12 files
31c8426 verified
Raw
History Blame Contribute Delete
42.3 kB
import cv2
import numpy as np
import mediapipe as mp
from collections import deque
from pathlib import Path
import tempfile
import os
import yt_dlp
import json
import requests
import math
# MediaPipe pose landmarks
LHIP, RHIP = 23, 24
LKNEE, RKNEE = 25, 26
LANKLE, RANKLE = 27, 28
LSHOULDER, RSHOULDER = 11, 12
NOSE = 0
POSE_CONNECTIONS = mp.solutions.pose.POSE_CONNECTIONS
# Jump performance standards (in cm) based on demographics
JUMP_STANDARDS = {
"Male": {
"average": 45, # Average jump height for males
"pro": 75 # Professional/elite level for males
},
"Female": {
"average": 35, # Average jump height for females
"pro": 65 # Professional/elite level for females
}
}
def smooth_moving_avg(series, k=5):
"""Simple causal moving average; ignores None values."""
out = []
q = deque()
s = 0.0
cnt = 0
for v in series:
if v is not None:
q.append(v)
s += v
cnt += 1
else:
q.append(None)
if len(q) > k:
old = q.popleft()
if old is not None:
s -= old
cnt -= 1
out.append((s / max(cnt, 1)) if cnt > 0 else None)
return out
def calculate_angle(point1, point2, point3):
"""Calculate angle between three points (point2 is the vertex)."""
# Calculate vectors
vec1 = np.array([point1.x - point2.x, point1.y - point2.y])
vec2 = np.array([point3.x - point2.x, point3.y - point2.y])
# Calculate angle using dot product
cos_angle = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
cos_angle = np.clip(cos_angle, -1.0, 1.0) # Handle numerical errors
angle = np.arccos(cos_angle)
return math.degrees(angle)
def analyze_knee_strain(landmarks, frame_height):
"""Analyze knee angles to detect strain and provide recommendations."""
if not landmarks:
return {"left_knee": None, "right_knee": None, "strain_detected": False}
lms = landmarks.landmark
# Calculate knee angles (hip-knee-ankle)
left_angle = None
right_angle = None
strain_detected = False
try:
# Left knee angle
left_angle = calculate_angle(lms[LHIP], lms[LKNEE], lms[LANKLE])
# Right knee angle
right_angle = calculate_angle(lms[RHIP], lms[RKNEE], lms[RANKLE])
# Check for strain (angles too acute indicate over-bending)
# Healthy knee angle during jumping should be > 90 degrees
# Angles < 70 degrees indicate potential strain
left_strain = left_angle < 70 if left_angle else False
right_strain = right_angle < 70 if right_angle else False
strain_detected = left_strain or right_strain
except (AttributeError, ZeroDivisionError):
pass
return {
"left_knee": {
"angle": left_angle,
"strain": left_angle < 70 if left_angle else False,
"optimal_angle": 90 # Recommended minimum angle
},
"right_knee": {
"angle": right_angle,
"strain": right_angle < 70 if right_angle else False,
"optimal_angle": 90
},
"strain_detected": strain_detected
}
def get_jump_reference_heights(gender, user_height_cm):
"""Get average and professional jump height references based on demographics."""
base_avg = JUMP_STANDARDS.get(gender, JUMP_STANDARDS["Male"])["average"]
base_pro = JUMP_STANDARDS.get(gender, JUMP_STANDARDS["Male"])["pro"]
# Adjust for height (taller people generally jump higher)
height_factor = user_height_cm / 175.0 # Normalize to average height
avg_height = base_avg * height_factor
pro_height = base_pro * height_factor
return {
"average": avg_height,
"professional": pro_height,
"gender": gender,
"height_adjusted": True
}
def draw_pose_landmarks(frame, landmarks, knee_analysis=None):
"""Draw pose landmarks and connections on the frame."""
if not landmarks:
return frame
h, w, _ = frame.shape
# Draw pose connections
mp_drawing = mp.solutions.drawing_utils
mp_pose = mp.solutions.pose
# Draw all pose landmarks
mp_drawing.draw_landmarks(
frame, landmarks, mp_pose.POSE_CONNECTIONS,
mp_drawing.DrawingSpec(color=(0, 255, 0), thickness=2, circle_radius=2),
mp_drawing.DrawingSpec(color=(0, 255, 255), thickness=2)
)
# Highlight knees with strain indicators
if knee_analysis and knee_analysis["strain_detected"]:
lms = landmarks.landmark
# Left knee
if knee_analysis["left_knee"]["strain"]:
left_knee_x = int(lms[LKNEE].x * w)
left_knee_y = int(lms[LKNEE].y * h)
cv2.circle(frame, (left_knee_x, left_knee_y), 8, (0, 0, 255), -1)
# Show angle and recommendation
angle_text = f"L: {knee_analysis['left_knee']['angle']:.0f}°"
cv2.putText(frame, angle_text, (left_knee_x - 30, left_knee_y - 15),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
cv2.putText(frame, "STRAIN!", (left_knee_x - 25, left_knee_y + 25),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 2)
# Right knee
if knee_analysis["right_knee"]["strain"]:
right_knee_x = int(lms[RKNEE].x * w)
right_knee_y = int(lms[RKNEE].y * h)
cv2.circle(frame, (right_knee_x, right_knee_y), 8, (0, 0, 255), -1)
# Show angle and recommendation
angle_text = f"R: {knee_analysis['right_knee']['angle']:.0f}°"
cv2.putText(frame, angle_text, (right_knee_x + 10, right_knee_y - 15),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
cv2.putText(frame, "STRAIN!", (right_knee_x + 5, right_knee_y + 25),
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 2)
return frame
def draw_jump_reference_lines(frame, references, current_jump_height, user_height_cm):
"""Draw average and professional jump height reference lines."""
h, w, _ = frame.shape
# Calculate line positions (relative to frame height)
# Assume the person's height spans about 70% of frame height
person_height_pixels = int(h * 0.7)
pixels_per_cm = person_height_pixels / user_height_cm
# Base line (ground level) - bottom 10% of frame
ground_y = int(h * 0.9)
# Reference lines
avg_jump_pixels = int(references["average"] * pixels_per_cm)
pro_jump_pixels = int(references["professional"] * pixels_per_cm)
current_jump_pixels = int(current_jump_height * pixels_per_cm)
avg_line_y = ground_y - avg_jump_pixels
pro_line_y = ground_y - pro_jump_pixels
current_line_y = ground_y - current_jump_pixels
# Draw ground line
cv2.line(frame, (0, ground_y), (w, ground_y), (100, 100, 100), 2)
cv2.putText(frame, "Ground", (10, ground_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (100, 100, 100), 2)
# Draw average line
if avg_line_y > 0:
cv2.line(frame, (0, avg_line_y), (w, avg_line_y), (255, 255, 0), 2)
cv2.putText(frame, f"Avg: {references['average']:.0f}cm",
(10, avg_line_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 0), 2)
# Draw professional line
if pro_line_y > 0:
cv2.line(frame, (0, pro_line_y), (w, pro_line_y), (0, 255, 0), 2)
cv2.putText(frame, f"Pro: {references['professional']:.0f}cm",
(10, pro_line_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
# Draw current jump line
if current_line_y > 0 and current_jump_height > 0:
cv2.line(frame, (0, current_line_y), (w, current_line_y), (0, 0, 255), 3)
cv2.putText(frame, f"Your Jump: {current_jump_height:.0f}cm",
(w - 200, current_line_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
return frame
def calculate_peak_power_output(jump_height_m, body_mass_kg, flight_time_s):
"""Calculate peak power output using biomechanical models."""
if jump_height_m <= 0 or flight_time_s <= 0:
return None
# Using the equation: Power = (body_mass * gravity * jump_height) / flight_time
# This is a simplified model - in reality, peak power occurs during takeoff phase
gravity = 9.81 # m/s²
# Average power during flight
avg_power = (body_mass_kg * gravity * jump_height_m) / (flight_time_s / 2)
# Peak power is typically 2-3x average power during explosive movements
peak_power = avg_power * 2.5
return peak_power # Watts
def calculate_rate_of_force_development(hip_y_series, fps, takeoff_start_idx, takeoff_end_idx):
"""Calculate Rate of Force Development from hip trajectory."""
if takeoff_end_idx <= takeoff_start_idx or len(hip_y_series) <= takeoff_end_idx:
return None
# Extract takeoff phase
takeoff_phase = hip_y_series[takeoff_start_idx:takeoff_end_idx + 1]
# Calculate velocity and acceleration
dt = 1.0 / fps
velocities = np.diff(takeoff_phase) / dt
accelerations = np.diff(velocities) / dt
if len(accelerations) == 0:
return None
# RFD is the maximum rate of change of force (approximated by acceleration)
# Convert to relative units (normalized by body position change)
max_acceleration = np.max(np.abs(accelerations))
# Normalize to get RFD index (higher values indicate faster force development)
rfd_index = max_acceleration * fps # per second
return rfd_index
def detect_ground_contact_phases(hip_y_series, fps, threshold_velocity=0.01):
"""Detect ground contact and flight phases from hip trajectory."""
if len(hip_y_series) < 5:
return [], []
# Calculate vertical velocity
velocities = np.diff(hip_y_series)
# Smooth velocities
velocities = smooth_moving_avg(list(velocities), k=3)
# Find phases where velocity is near zero (ground contact)
ground_contact_frames = []
flight_frames = []
for i, vel in enumerate(velocities):
if vel is not None and abs(vel) < threshold_velocity:
ground_contact_frames.append(i)
elif vel is not None:
flight_frames.append(i)
# Calculate ground contact time
if ground_contact_frames:
total_contact_frames = len(ground_contact_frames)
ground_contact_time = total_contact_frames / fps
else:
ground_contact_time = 0.0
return ground_contact_frames, ground_contact_time
def calculate_impulse_and_force(hip_y_series, fps, body_mass_kg, takeoff_start_idx, takeoff_end_idx):
"""Calculate impulse and force characteristics during takeoff."""
if takeoff_end_idx <= takeoff_start_idx or len(hip_y_series) <= takeoff_end_idx:
return None, None
# Extract takeoff phase
takeoff_phase = hip_y_series[takeoff_start_idx:takeoff_end_idx + 1]
dt = 1.0 / fps
# Calculate velocity and acceleration
velocities = np.diff(takeoff_phase) / dt
accelerations = np.diff(velocities) / dt
if len(accelerations) == 0:
return None, None
# Estimate force (F = ma, where a includes gravity)
gravity = 9.81
forces = body_mass_kg * (np.array(accelerations) + gravity)
# Calculate impulse (area under force-time curve)
impulse = np.trapz(forces, dx=dt)
# Peak force
peak_force = np.max(forces) if len(forces) > 0 else None
return impulse, peak_force
def estimate_jump_metrics(hip_y_series, fps, body_mass_kg=75.0):
"""Return comprehensive jump metrics including advanced biomechanical parameters."""
# Remove None values
hip_clean = [(i, h) for i, h in enumerate(hip_y_series) if h is not None]
if len(hip_clean) < 5:
return None
indices, hip_values = zip(*hip_clean)
hip = list(hip_values)
# Smooth the data
hip_smooth = smooth_moving_avg(hip, k=5)
# Basic jump metrics
min_y = min(hip_smooth) # apex (body highest)
max_y = max(hip_smooth) # deepest crouch (body lowest)
jump_height_norm = max(0.0, (max_y - min_y))
# Find key phase indices
hip_arr = np.array(hip_smooth, dtype=float)
vel = np.diff(hip_arr)
if vel.size == 0:
return None
# Identify takeoff and landing phases
takeoff_idx = int(np.argmin(vel)) # most negative velocity (takeoff)
landing_idx = int(np.argmax(vel)) # most positive velocity (landing)
# Flight time
flight_frames = max(0, landing_idx - takeoff_idx)
flight_time_s = flight_frames / float(fps or 30.0)
# Find takeoff phase (from crouch to takeoff)
crouch_idx = int(np.argmax(hip_smooth)) # deepest crouch
takeoff_start_idx = max(0, crouch_idx - 10) # start of takeoff phase
takeoff_end_idx = takeoff_idx
# Advanced metrics
jump_height_m = jump_height_norm * 2.0 # Rough conversion to meters
# Peak Power Output
peak_power = calculate_peak_power_output(jump_height_m, body_mass_kg, flight_time_s)
# Rate of Force Development
rfd = calculate_rate_of_force_development(hip_smooth, fps, takeoff_start_idx, takeoff_end_idx)
# Ground Contact Time
ground_contact_frames, ground_contact_time = detect_ground_contact_phases(hip_smooth, fps)
# Impulse and Force
impulse, peak_force = calculate_impulse_and_force(hip_smooth, fps, body_mass_kg, takeoff_start_idx, takeoff_end_idx)
return {
'jump_height_norm': jump_height_norm,
'flight_time_s': flight_time_s,
'peak_power_watts': peak_power,
'rate_of_force_development': rfd,
'ground_contact_time_s': ground_contact_time,
'impulse_ns': impulse,
'peak_force_n': peak_force,
'takeoff_phase_duration_s': (takeoff_end_idx - takeoff_start_idx) / fps if takeoff_end_idx > takeoff_start_idx else 0
}
def download_youtube_video(youtube_url, output_path):
"""Download YouTube video to specified path."""
ydl_opts = {
'format': 'best[height<=720]', # Limit quality for faster processing
'outtmpl': output_path,
'quiet': True,
'no_warnings': True,
}
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
ydl.download([youtube_url])
return output_path
def generate_annotated_video(video_path, user_height_cm, user_weight_kg, gender, output_path=None, progress_callback=None):
"""Generate annotated video with pose tracking, jump analysis, and knee strain detection."""
# Set up output path
if output_path is None:
video_name = Path(video_path).stem
output_path = f"{video_name}_annotated.mp4"
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise Exception(f"Could not open video: {video_path}")
# Get video properties
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
# Set up video writer
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(output_path, fourcc, fps, (w, h))
# Set up pose detection
mp_pose = mp.solutions.pose
pose = mp_pose.Pose(static_image_mode=False, model_complexity=1, enable_segmentation=False)
# Get jump references
jump_references = get_jump_reference_heights(gender, user_height_cm)
# Track hip positions for jump height calculation
hip_y_series = []
frame_idx = 0
print(f"Generating annotated video: {output_path}")
print(f"Video dimensions: {w}x{h}, FPS: {fps}, Total frames: {total_frames}")
while True:
ret, frame = cap.read()
if not ret:
break
# Process frame for pose detection
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
results = pose.process(rgb)
# Track hip position
hip_y = None
if results.pose_landmarks:
lms = results.pose_landmarks.landmark
hip_y = (lms[LHIP].y + lms[RHIP].y) / 2.0
hip_y_series.append(hip_y)
else:
hip_y_series.append(None)
# Calculate current jump height (rough estimate)
current_jump_height = 0
if len(hip_y_series) > 10: # Need some history
recent_hips = [h for h in hip_y_series[-20:] if h is not None]
if recent_hips:
min_hip = min(recent_hips)
max_hip = max(recent_hips)
normalized_jump = max_hip - min_hip
current_jump_height = normalized_jump * user_height_cm
# Analyze knee strain
knee_analysis = analyze_knee_strain(results.pose_landmarks, h)
# Draw pose landmarks with strain indicators
annotated_frame = draw_pose_landmarks(frame, results.pose_landmarks, knee_analysis)
# Draw jump reference lines
annotated_frame = draw_jump_reference_lines(
annotated_frame, jump_references, current_jump_height, user_height_cm
)
# Add performance info overlay
info_y = 30
cv2.putText(annotated_frame, f"Frame: {frame_idx}/{total_frames}",
(10, info_y), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
if current_jump_height > 0:
cv2.putText(annotated_frame, f"Current Jump: {current_jump_height:.1f}cm",
(10, info_y + 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
# Add knee strain warnings
if knee_analysis["strain_detected"]:
warning_text = "⚠️ KNEE STRAIN DETECTED!"
cv2.putText(annotated_frame, warning_text, (10, h - 60),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
recommendations = "Keep knees above 90° angle"
cv2.putText(annotated_frame, recommendations, (10, h - 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2)
# Write frame to output video
out.write(annotated_frame)
frame_idx += 1
# Update progress
if progress_callback and total_frames > 0:
progress = min(frame_idx / total_frames, 1.0)
progress_callback(progress, f"Processing frame {frame_idx}/{total_frames}")
# Cleanup
cap.release()
out.release()
# Calculate final jump metrics
jump_metrics = estimate_jump_metrics(hip_y_series, fps, user_weight_kg)
print(f"Annotated video saved: {output_path}")
return {
"output_video_path": output_path,
"jump_metrics": jump_metrics,
"jump_references": jump_references,
"total_frames_processed": frame_idx,
"knee_strain_detected": any(analyze_knee_strain(None, h)["strain_detected"] for _ in range(5)) # Simplified check
}
def process_video_analysis(video_path, user_height_cm, user_weight_kg=75.0, progress_callback=None):
"""Core video analysis function with progress tracking.
Args:
video_path (str): Path to the video file
user_height_cm (float): User's height in centimeters
user_weight_kg (float): User's weight in kilograms (default: 75kg)
progress_callback (callable, optional): Function to call with progress updates
Signature: progress_callback(progress_float, description_string)
Returns:
dict: Analysis results containing jump metrics and video info
None: If analysis failed
"""
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise Exception(f"Could not open video: {video_path}")
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
mp_pose = mp.solutions.pose
pose = mp_pose.Pose(static_image_mode=False, model_complexity=1, enable_segmentation=False)
hip_y_series = []
frame_idx = 0
print(f"Processing video: {Path(video_path).name}")
print(f"Video dimensions: {w}x{h}, FPS: {fps}, Total frames: {total_frames}")
ok, frame = cap.read()
while ok:
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
res = pose.process(rgb)
if res.pose_landmarks:
lms = res.pose_landmarks.landmark
mid_hip_y = (lms[LHIP].y + lms[RHIP].y) / 2.0
hip_y_series.append(float(mid_hip_y))
else:
hip_y_series.append(None)
frame_idx += 1
# Update progress
if progress_callback and total_frames > 0:
progress = min(frame_idx / total_frames, 1.0)
progress_callback(progress, f"Processing frame {frame_idx}/{total_frames}")
ok, frame = cap.read()
cap.release()
print(f"Completed processing {frame_idx} frames")
# Calculate comprehensive jump metrics
metrics = estimate_jump_metrics(hip_y_series, fps, user_weight_kg)
if metrics is None:
return None
# Convert normalized jump height to actual height in cm
jump_height_cm = metrics['jump_height_norm'] * user_height_cm
return {
"video": Path(video_path).name,
"frames": len(hip_y_series),
"fps": fps,
"jump_height_cm": jump_height_cm,
"normalized_rise": metrics['jump_height_norm'],
"flight_time_s": metrics['flight_time_s'],
"peak_power_watts": metrics['peak_power_watts'],
"rate_of_force_development": metrics['rate_of_force_development'],
"ground_contact_time_s": metrics['ground_contact_time_s'],
"impulse_ns": metrics['impulse_ns'],
"peak_force_n": metrics['peak_force_n'],
"takeoff_phase_duration_s": metrics['takeoff_phase_duration_s'],
"user_weight_kg": user_weight_kg
}
def analyze_youtube_video(youtube_url, user_height_cm, user_weight_kg=75.0, progress_callback=None):
"""Analyze jump from YouTube video.
Args:
youtube_url (str): YouTube video URL
user_height_cm (float): User's height in centimeters
user_weight_kg (float): User's weight in kilograms (default: 75kg)
progress_callback (callable, optional): Function to call with progress updates
Returns:
dict: Analysis results or error information
"""
# Validate inputs
if not youtube_url or not youtube_url.strip():
return {"error": "Please provide a YouTube URL"}
if not user_height_cm or user_height_cm <= 0:
return {"error": "Please provide a valid height in centimeters"}
try:
if progress_callback:
progress_callback(0.1, "Validating YouTube URL...")
# Validate YouTube URL
youtube_url = youtube_url.strip()
if not any(domain in youtube_url for domain in ['youtube.com', 'youtu.be']):
return {"error": "Please provide a valid YouTube URL"}
# Create temporary directory for processing
with tempfile.TemporaryDirectory() as temp_dir:
if progress_callback:
progress_callback(0.2, "Downloading video from YouTube...")
# Download video
video_filename = os.path.join(temp_dir, 'video.%(ext)s')
try:
download_youtube_video(youtube_url, video_filename)
# Find the actual downloaded file
video_files = [f for f in os.listdir(temp_dir) if f.startswith('video.')]
if not video_files:
return {"error": "Failed to download YouTube video. Please check the URL and try again."}
video_path = os.path.join(temp_dir, video_files[0])
except Exception as e:
return {"error": f"Failed to download YouTube video: {str(e)}"}
if progress_callback:
progress_callback(0.3, "Starting video analysis...")
# Process the video with progress tracking
def update_progress(prog, desc):
if progress_callback:
progress_callback(0.3 + (prog * 0.6), desc)
result = process_video_analysis(video_path, user_height_cm, user_weight_kg, update_progress)
if progress_callback:
progress_callback(0.9, "Analysis complete!")
return result
except Exception as e:
return {"error": f"Error during analysis: {str(e)}"}
def analyze_video_file(video_path, user_height_cm, user_weight_kg=75.0, progress_callback=None):
"""Analyze jump from video file.
Args:
video_path (str): Path to video file
user_height_cm (float): User's height in centimeters
user_weight_kg (float): User's weight in kilograms (default: 75kg)
progress_callback (callable, optional): Function to call with progress updates
Returns:
dict: Analysis results or error information
"""
# Validate inputs
if not video_path:
return {"error": "Please provide a video file"}
if not user_height_cm or user_height_cm <= 0:
return {"error": "Please provide a valid height in centimeters"}
try:
if progress_callback:
progress_callback(0.1, "Processing video file...")
# Process the video with progress tracking
def update_progress(prog, desc):
if progress_callback:
progress_callback(0.1 + (prog * 0.8), desc)
result = process_video_analysis(video_path, user_height_cm, user_weight_kg, update_progress)
if progress_callback:
progress_callback(1.0, "Analysis complete!")
return result
except Exception as e:
return {"error": f"Error during analysis: {str(e)}"}
def generate_annotated_video_from_youtube(youtube_url, user_height_cm, user_weight_kg, gender, progress_callback=None):
"""Generate annotated video from YouTube URL."""
# Validate inputs
if not youtube_url or not youtube_url.strip():
return {"error": "Please provide a YouTube URL"}
if not user_height_cm or user_height_cm <= 0:
return {"error": "Please provide a valid height in centimeters"}
try:
if progress_callback:
progress_callback(0.1, "Downloading YouTube video...")
# Validate YouTube URL
youtube_url = youtube_url.strip()
if not any(domain in youtube_url for domain in ['youtube.com', 'youtu.be']):
return {"error": "Please provide a valid YouTube URL"}
# Create temporary directory for processing
with tempfile.TemporaryDirectory() as temp_dir:
if progress_callback:
progress_callback(0.2, "Downloading video from YouTube...")
# Download video
video_filename = os.path.join(temp_dir, 'video.%(ext)s')
try:
download_youtube_video(youtube_url, video_filename)
# Find the actual downloaded file
video_files = [f for f in os.listdir(temp_dir) if f.startswith('video.')]
if not video_files:
return {"error": "Failed to download YouTube video. Please check the URL and try again."}
video_path = os.path.join(temp_dir, video_files[0])
except Exception as e:
return {"error": f"Failed to download YouTube video: {str(e)}"}
if progress_callback:
progress_callback(0.3, "Generating annotated video...")
# Generate output path in temp directory
output_path = os.path.join(temp_dir, "annotated_output.mp4")
# Process the video with progress tracking
def update_progress(prog, desc):
if progress_callback:
progress_callback(0.3 + (prog * 0.6), desc)
result = generate_annotated_video(
video_path, user_height_cm, user_weight_kg, gender,
output_path, update_progress
)
if progress_callback:
progress_callback(0.95, "Finalizing annotated video...")
# Move the output file to a permanent location
final_output = f"annotated_jump_analysis_{Path(youtube_url).stem}.mp4"
if os.path.exists(output_path):
# In production, you'd save this to a proper storage location
result["output_video_path"] = output_path
result["download_ready"] = True
if progress_callback:
progress_callback(1.0, "Annotated video generation complete!")
return result
except Exception as e:
return {"error": f"Error during video generation: {str(e)}"}
def generate_annotated_video_from_file(video_file_path, user_height_cm, user_weight_kg, gender, progress_callback=None):
"""Generate annotated video from uploaded file."""
# Validate inputs
if not video_file_path:
return {"error": "Please provide a video file"}
if not user_height_cm or user_height_cm <= 0:
return {"error": "Please provide a valid height in centimeters"}
try:
if progress_callback:
progress_callback(0.1, "Processing uploaded video...")
# Generate output path
video_name = Path(video_file_path).stem
output_path = f"{video_name}_annotated.mp4"
# Process the video with progress tracking
def update_progress(prog, desc):
if progress_callback:
progress_callback(0.1 + (prog * 0.8), desc)
result = generate_annotated_video(
video_file_path, user_height_cm, user_weight_kg, gender,
output_path, update_progress
)
if progress_callback:
progress_callback(1.0, "Annotated video generation complete!")
return result
except Exception as e:
return {"error": f"Error during video generation: {str(e)}"}
def get_performance_insights(result_dict):
"""Generate performance insights based on comprehensive jump metrics.
Args:
result_dict (dict): Dictionary containing all jump analysis results
Returns:
list: List of insight strings
"""
insights = []
jump_height_cm = result_dict.get('jump_height_cm', 0)
flight_time_s = result_dict.get('flight_time_s', 0)
peak_power_watts = result_dict.get('peak_power_watts')
rfd = result_dict.get('rate_of_force_development')
ground_contact_time = result_dict.get('ground_contact_time_s')
peak_force = result_dict.get('peak_force_n')
# Jump height insights
if jump_height_cm > 60:
insights.append("🔥 **Excellent jump height!** This is above average performance.")
elif jump_height_cm > 40:
insights.append("👍 **Good jump height!** Solid athletic performance.")
elif jump_height_cm > 25:
insights.append("📈 **Moderate jump height.** Room for improvement with training.")
else:
insights.append("🎯 **Starting point identified.** Focus on technique and strength training.")
# Flight time insights
if flight_time_s > 0.5:
insights.append("⏱️ **Great flight time!** Shows good explosive power.")
elif flight_time_s > 0.3:
insights.append("⏱️ **Decent flight time.** Good coordination.")
# Peak power insights
if peak_power_watts and peak_power_watts > 3000:
insights.append("⚡ **Outstanding power output!** Elite-level explosive strength.")
elif peak_power_watts and peak_power_watts > 2000:
insights.append("💪 **High power output!** Strong explosive capabilities.")
elif peak_power_watts and peak_power_watts > 1000:
insights.append("🏋️ **Moderate power output.** Good base strength to build on.")
# Rate of Force Development insights
if rfd and rfd > 15:
insights.append("🚀 **Excellent RFD!** Very quick force generation - great for sprinting and jumping.")
elif rfd and rfd > 10:
insights.append("⚡ **Good RFD!** Solid ability to generate force quickly.")
elif rfd and rfd > 5:
insights.append("📊 **Moderate RFD.** Work on plyometric training to improve explosiveness.")
# Ground contact time insights
if ground_contact_time and ground_contact_time < 0.2:
insights.append("🦘 **Excellent ground contact time!** Very efficient stretch-shortening cycle.")
elif ground_contact_time and ground_contact_time < 0.3:
insights.append("👟 **Good ground contact efficiency.** Solid reactive strength.")
# Peak force insights
if peak_force and peak_force > 2000:
insights.append("🏋️‍♂️ **High peak force production!** Strong neuromuscular system.")
elif peak_force and peak_force > 1500:
insights.append("💪 **Good force production.** Solid strength foundation.")
return insights
def test_gemini_api_connection(api_key):
"""Test the Gemini API connection with a simple request."""
if not api_key:
return {"error": "No API key provided"}
url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent"
headers = {
'Content-Type': 'application/json',
'X-goog-api-key': api_key.strip()
}
# Simple test data
test_data = {
"contents": [
{
"parts": [
{
"text": "Say hello in exactly 5 words."
}
]
}
],
"generationConfig": {
"temperature": 0.1,
"maxOutputTokens": 20
}
}
try:
response = requests.post(url, headers=headers, json=test_data, timeout=10)
return {
"status_code": response.status_code,
"response_text": response.text[:500],
"success": response.status_code == 200,
"error": None if response.status_code == 200 else f"HTTP {response.status_code}"
}
except Exception as e:
return {
"status_code": None,
"response_text": str(e),
"success": False,
"error": str(e)
}
def get_ai_sports_coaching_analysis(jump_height_cm, user_height_cm, gender, favorite_sports,
peak_power_watts=None, flight_time_s=None, rfd=None, api_key=None):
"""Get AI-powered sports coaching analysis using Google Gemini API.
Args:
jump_height_cm (float): Jump height in centimeters
user_height_cm (float): User's height in centimeters
gender (str): User's gender ('Male' or 'Female')
favorite_sports (list): List of user's favorite sports
peak_power_watts (float, optional): Peak power output in watts
flight_time_s (float, optional): Flight time in seconds
rfd (float, optional): Rate of force development
api_key (str): Google Gemini API key
Returns:
dict: AI analysis with percentile rankings and improvement suggestions
"""
if not api_key:
return {"error": "Gemini API key is required"}
# Calculate relative jump height
relative_jump_height = (jump_height_cm / user_height_cm) * 100 if user_height_cm > 0 else 0
# Prepare the analysis prompt
# Handle None values safely
flight_time_str = f"{flight_time_s:.3f}" if flight_time_s is not None else "N/A"
peak_power_str = f"{peak_power_watts:.0f}" if peak_power_watts is not None else "N/A"
rfd_str = f"{rfd:.2f}" if rfd is not None else "N/A"
# Format favorite sports list
sports_list = ", ".join(favorite_sports) if favorite_sports else "None specified"
prompt = f"""You are a sports coach. Based on this athlete's jump performance, provide analysis in JSON format.
ATHLETE DATA:
- Gender: {gender}
- Height: {user_height_cm} cm
- Jump Height: {jump_height_cm:.2f} cm
- Sports: {sports_list}
RESPOND WITH VALID JSON IN THIS EXACT FORMAT:
{{
"sports": {{
"Basketball": 75,
"Volleyball": 80
}},
"tips": [
"Focus on proper landing technique to reduce knee strain",
"Add plyometric exercises to your training routine",
"Strengthen your leg muscles with squats and lunges",
"Practice explosive movements for better power development"
]
}}
RULES:
- "sports" should contain percentile rankings (0-100) only for sports in their list: {sports_list}
- "tips" must be exactly 4 practical improvement suggestions
- Use simple English for beginner athletes
- Return ONLY valid JSON, no extra text, no markdown formatting, no code blocks"""
# Prepare the API request - try Gemini 1.5 Pro as fallback
url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent"
headers = {
'Content-Type': 'application/json',
'X-goog-api-key': api_key
}
data = {
"contents": [
{
"parts": [
{
"text": prompt
}
]
}
],
"generationConfig": {
"temperature": 0.7,
"topK": 40,
"topP": 0.95,
"maxOutputTokens": 1024
}
}
try:
response = requests.post(url, headers=headers, json=data, timeout=30)
# Enhanced error handling for debugging
if response.status_code == 403:
return {
"error": f"API key authentication failed (403). Please verify:\n"
f"1. API key is correct and active\n"
f"2. Generative AI API is enabled in Google Cloud Console\n"
f"3. Billing is set up for your Google Cloud project\n"
f"4. API key has proper permissions\n"
f"Response: {response.text[:200]}..."
}
elif response.status_code == 429:
return {"error": "Rate limit exceeded. Please try again later."}
elif response.status_code == 400:
return {"error": f"Bad request (400). Response: {response.text[:200]}..."}
response.raise_for_status()
result = response.json()
if 'candidates' in result and len(result['candidates']) > 0:
ai_response_text = result['candidates'][0]['content']['parts'][0]['text']
try:
# Clean the response text - remove markdown code blocks if present
cleaned_response = ai_response_text.strip()
# Remove ```json and ``` markers if they exist
if cleaned_response.startswith('```json'):
cleaned_response = cleaned_response[7:] # Remove ```json
if cleaned_response.startswith('```'):
cleaned_response = cleaned_response[3:] # Remove ```
if cleaned_response.endswith('```'):
cleaned_response = cleaned_response[:-3] # Remove trailing ```
cleaned_response = cleaned_response.strip()
# Parse the JSON response from AI
ai_analysis_json = json.loads(cleaned_response)
# Validate the expected structure
if not isinstance(ai_analysis_json, dict):
raise ValueError("Response is not a JSON object")
# Extract required fields with defaults
sports_percentiles = ai_analysis_json.get('sports', {})
tips = ai_analysis_json.get('tips', [])
# Ensure tips is a list
if not isinstance(tips, list):
tips = []
# Ensure sports is a dict
if not isinstance(sports_percentiles, dict):
sports_percentiles = {}
return {
"success": True,
"analysis": {
"sports": sports_percentiles,
"tips": tips
},
"athlete_profile": {
"gender": gender,
"height_cm": user_height_cm,
"jump_height_cm": jump_height_cm,
"relative_jump_height": relative_jump_height,
"flight_time_s": flight_time_s,
"peak_power_watts": peak_power_watts,
"rfd": rfd
}
}
except json.JSONDecodeError as e:
# Fallback: if JSON parsing fails, try to extract some information
return {
"error": f"AI returned invalid JSON. Raw response: {ai_response_text[:200]}...",
"parsing_error": str(e)
}
except Exception as e:
return {
"error": f"Error processing AI response: {str(e)}",
"raw_response": ai_response_text[:200]
}
else:
return {"error": f"No response generated from AI. Response: {result}"}
except requests.exceptions.RequestException as e:
return {"error": f"API request failed: {str(e)}"}
except json.JSONDecodeError as e:
return {"error": f"Failed to parse API response: {str(e)}"}
except Exception as e:
return {"error": f"Unexpected error: {str(e)}"}