akarsh999 commited on
Commit
858ce7d
·
verified ·
1 Parent(s): f7d536c

Upload 9 files

Browse files
Files changed (2) hide show
  1. app.py +176 -3
  2. athletic_performance.py +422 -0
app.py CHANGED
@@ -1,7 +1,11 @@
1
  import gradio as gr
2
  import pandas as pd
3
  import os
4
- from athletic_performance import analyze_youtube_video, analyze_video_file, get_performance_insights, get_ai_sports_coaching_analysis, test_gemini_api_connection
 
 
 
 
5
 
6
  def analyze_jump_from_youtube(youtube_url, user_height_cm, user_weight_kg, progress=gr.Progress()):
7
  """Main analysis function for Gradio interface."""
@@ -280,6 +284,118 @@ def test_api_key(api_key):
280
  else:
281
  return f"❌ API Key test failed!\n\nStatus Code: {result['status_code']}\nError: {result['error']}\n\nResponse: {result['response_text']}"
282
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  # Create Gradio interface
284
  def create_interface():
285
  with gr.Blocks(title="🏃‍♂️ Athletic Ability Analysis") as app:
@@ -292,13 +408,18 @@ def create_interface():
292
  ## 🚀 Features
293
  - **📊 Biomechanical Analysis**: Comprehensive jump metrics (height, power, force, RFD)
294
  - **🤖 AI Sports Coach**: Personalized sport recommendations and technique improvements
 
 
295
  - **🎯 Performance Insights**: Professional-grade analysis and training suggestions
296
 
297
  ## 📋 Instructions
298
  1. Enter your height in centimeters and weight in kilograms
299
- 2. Choose your analysis type: Basic metrics, or AI coaching with sports recommendations
 
 
 
300
  3. Provide a video (YouTube URL or file upload)
301
- 4. Get comprehensive results and actionable insights
302
  """)
303
 
304
  with gr.Row():
@@ -393,6 +514,45 @@ def create_interface():
393
  gr.Markdown("*Provide either a YouTube URL or upload a video file*")
394
 
395
  ai_coaching_btn = gr.Button("🤖 Get AI Coaching Analysis", variant="primary", size="lg")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
 
397
  # Results section
398
  gr.Markdown("## 📊 Analysis Results")
@@ -466,6 +626,19 @@ def create_interface():
466
  outputs=[api_test_result]
467
  )
468
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  # Example section
470
  gr.Examples(
471
  examples=[
 
1
  import gradio as gr
2
  import pandas as pd
3
  import os
4
+ from athletic_performance import (
5
+ analyze_youtube_video, analyze_video_file, get_performance_insights,
6
+ get_ai_sports_coaching_analysis, test_gemini_api_connection,
7
+ generate_annotated_video_from_youtube, generate_annotated_video_from_file
8
+ )
9
 
10
  def analyze_jump_from_youtube(youtube_url, user_height_cm, user_weight_kg, progress=gr.Progress()):
11
  """Main analysis function for Gradio interface."""
 
284
  else:
285
  return f"❌ API Key test failed!\n\nStatus Code: {result['status_code']}\nError: {result['error']}\n\nResponse: {result['response_text']}"
286
 
287
+
288
+ def generate_video_from_youtube(youtube_url, user_height_cm, user_weight_kg, gender, progress=gr.Progress()):
289
+ """Generate annotated video from YouTube URL."""
290
+
291
+ # Create progress callback
292
+ def progress_callback(prog, desc):
293
+ progress(prog, desc=desc)
294
+
295
+ # Call the video generation function
296
+ result = generate_annotated_video_from_youtube(
297
+ youtube_url, user_height_cm, user_weight_kg, gender, progress_callback
298
+ )
299
+
300
+ # Handle errors
301
+ if "error" in result:
302
+ return f"❌ Video generation failed: {result['error']}", None, None
303
+
304
+ if result is None:
305
+ return "❌ Could not generate video. Please ensure the video shows a clear vertical jump.", None, None
306
+
307
+ # Format results
308
+ video_path = result.get("output_video_path", "")
309
+ jump_metrics = result.get("jump_metrics", {})
310
+
311
+ results_text = f"""
312
+ # 🎬 Annotated Video Generated!
313
+
314
+ ## 📊 Jump Analysis Summary
315
+ - **Jump Height**: {jump_metrics.get('jump_height_cm', 0):.2f} cm
316
+ - **Flight Time**: {jump_metrics.get('flight_time_s', 0):.3f} seconds
317
+ - **Peak Power**: {jump_metrics.get('peak_power_watts', 0):.0f} watts
318
+ - **Frames Processed**: {result.get('total_frames_processed', 0)}
319
+
320
+ ## 🎥 Video Features Added
321
+ - ✅ **Pose Tracking**: Real-time skeleton overlay
322
+ - ✅ **Jump Reference Lines**: Average vs Professional heights
323
+ - ✅ **Knee Strain Detection**: Red markers for poor form
324
+ - ✅ **Performance Metrics**: Live jump height tracking
325
+
326
+ ## 📥 Download
327
+ Your annotated video is ready for download!
328
+ """
329
+
330
+ # Create summary dataframe
331
+ summary_df = pd.DataFrame([
332
+ ["Jump Height", f"{jump_metrics.get('jump_height_cm', 0):.2f} cm"],
333
+ ["Flight Time", f"{jump_metrics.get('flight_time_s', 0):.3f} seconds"],
334
+ ["Peak Power", f"{jump_metrics.get('peak_power_watts', 0):.0f} watts"],
335
+ ["Video Features", "Pose + References + Strain Detection"],
336
+ ["Output Format", "MP4 Video"],
337
+ ["Status", "✅ Ready for Download"],
338
+ ], columns=["Metric", "Value"])
339
+
340
+ return results_text, summary_df, video_path
341
+
342
+
343
+ def generate_video_from_file(video_file, user_height_cm, user_weight_kg, gender, progress=gr.Progress()):
344
+ """Generate annotated video from uploaded file."""
345
+
346
+ # Create progress callback
347
+ def progress_callback(prog, desc):
348
+ progress(prog, desc=desc)
349
+
350
+ # Call the video generation function
351
+ video_path = video_file.name if video_file else None
352
+ result = generate_annotated_video_from_file(
353
+ video_path, user_height_cm, user_weight_kg, gender, progress_callback
354
+ )
355
+
356
+ # Handle errors
357
+ if "error" in result:
358
+ return f"❌ Video generation failed: {result['error']}", None, None
359
+
360
+ if result is None:
361
+ return "❌ Could not generate video. Please ensure the video shows a clear vertical jump.", None, None
362
+
363
+ # Format results (same as YouTube function)
364
+ video_path = result.get("output_video_path", "")
365
+ jump_metrics = result.get("jump_metrics", {})
366
+
367
+ results_text = f"""
368
+ # 🎬 Annotated Video Generated!
369
+
370
+ ## 📊 Jump Analysis Summary
371
+ - **Jump Height**: {jump_metrics.get('jump_height_cm', 0):.2f} cm
372
+ - **Flight Time**: {jump_metrics.get('flight_time_s', 0):.3f} seconds
373
+ - **Peak Power**: {jump_metrics.get('peak_power_watts', 0):.0f} watts
374
+ - **Frames Processed**: {result.get('total_frames_processed', 0)}
375
+
376
+ ## 🎥 Video Features Added
377
+ - ✅ **Pose Tracking**: Real-time skeleton overlay
378
+ - ✅ **Jump Reference Lines**: Average vs Professional heights
379
+ - ✅ **Knee Strain Detection**: Red markers for poor form
380
+ - ✅ **Performance Metrics**: Live jump height tracking
381
+
382
+ ## 📥 Download
383
+ Your annotated video is ready for download!
384
+ """
385
+
386
+ # Create summary dataframe
387
+ summary_df = pd.DataFrame([
388
+ ["Jump Height", f"{jump_metrics.get('jump_height_cm', 0):.2f} cm"],
389
+ ["Flight Time", f"{jump_metrics.get('flight_time_s', 0):.3f} seconds"],
390
+ ["Peak Power", f"{jump_metrics.get('peak_power_watts', 0):.0f} watts"],
391
+ ["Video Features", "Pose + References + Strain Detection"],
392
+ ["Output Format", "MP4 Video"],
393
+ ["Status", "✅ Ready for Download"],
394
+ ], columns=["Metric", "Value"])
395
+
396
+ return results_text, summary_df, video_path
397
+
398
+
399
  # Create Gradio interface
400
  def create_interface():
401
  with gr.Blocks(title="🏃‍♂️ Athletic Ability Analysis") as app:
 
408
  ## 🚀 Features
409
  - **📊 Biomechanical Analysis**: Comprehensive jump metrics (height, power, force, RFD)
410
  - **🤖 AI Sports Coach**: Personalized sport recommendations and technique improvements
411
+ - **🎬 Annotated Videos**: Generate training videos with pose tracking and performance overlays
412
+ - **⚠️ Technique Analysis**: Real-time knee strain detection and form corrections
413
  - **🎯 Performance Insights**: Professional-grade analysis and training suggestions
414
 
415
  ## 📋 Instructions
416
  1. Enter your height in centimeters and weight in kilograms
417
+ 2. Choose your analysis type:
418
+ - **📊 Standard Analysis**: Get detailed biomechanical metrics
419
+ - **🤖 AI Sports Coach**: Personalized recommendations and sport suggestions
420
+ - **🎬 Video Generation**: Create annotated training videos with visual overlays
421
  3. Provide a video (YouTube URL or file upload)
422
+ 4. Get comprehensive results, actionable insights, or downloadable training videos
423
  """)
424
 
425
  with gr.Row():
 
514
  gr.Markdown("*Provide either a YouTube URL or upload a video file*")
515
 
516
  ai_coaching_btn = gr.Button("🤖 Get AI Coaching Analysis", variant="primary", size="lg")
517
+
518
+ # Video Generation Tab
519
+ with gr.TabItem("🎬 Annotated Video"):
520
+ gr.Markdown("""
521
+ ## 🎬 Generate Annotated Training Video
522
+
523
+ Create a professional training video with visual overlays including:
524
+ - **🦴 Pose Tracking**: Real-time skeleton visualization
525
+ - **📏 Performance Lines**: Average vs Professional jump heights
526
+ - **⚠️ Knee Strain Detection**: Red warnings for poor form
527
+ - **📊 Live Metrics**: Frame-by-frame jump analysis
528
+
529
+ Perfect for coaches, athletes, and performance analysis!
530
+ """)
531
+
532
+ with gr.Row():
533
+ with gr.Column():
534
+ video_gender = gr.Radio(
535
+ choices=["Male", "Female"],
536
+ label="Gender (for performance references)",
537
+ value="Male"
538
+ )
539
+ gr.Markdown("*Used to set appropriate average/pro jump height lines*")
540
+
541
+ with gr.Column():
542
+ gr.Markdown("### Video Input Options")
543
+ video_youtube_url = gr.Textbox(
544
+ label="YouTube URL (Option 1)",
545
+ placeholder="https://youtube.com/watch?v=..."
546
+ )
547
+ video_file_upload = gr.File(
548
+ label="Upload Video File (Option 2)",
549
+ file_types=[".mp4", ".avi", ".mov", ".mkv", ".webm"]
550
+ )
551
+ gr.Markdown("*Provide either a YouTube URL or upload a video file*")
552
+
553
+ with gr.Row():
554
+ video_youtube_btn = gr.Button("🎬 Generate from YouTube", variant="primary", size="lg")
555
+ video_file_btn = gr.Button("🎬 Generate from Upload", variant="primary", size="lg")
556
 
557
  # Results section
558
  gr.Markdown("## 📊 Analysis Results")
 
626
  outputs=[api_test_result]
627
  )
628
 
629
+ # Video generation event handlers
630
+ video_youtube_btn.click(
631
+ fn=generate_video_from_youtube,
632
+ inputs=[video_youtube_url, user_height, user_weight, video_gender],
633
+ outputs=[results_text, results_table, gr.File(label="Download Video")]
634
+ )
635
+
636
+ video_file_btn.click(
637
+ fn=generate_video_from_file,
638
+ inputs=[video_file_upload, user_height, user_weight, video_gender],
639
+ outputs=[results_text, results_table, gr.File(label="Download Video")]
640
+ )
641
+
642
  # Example section
643
  gr.Examples(
644
  examples=[
athletic_performance.py CHANGED
@@ -8,11 +8,28 @@ import os
8
  import yt_dlp
9
  import json
10
  import requests
 
11
 
12
  # MediaPipe pose landmarks
13
  LHIP, RHIP = 23, 24
 
 
 
 
14
  POSE_CONNECTIONS = mp.solutions.pose.POSE_CONNECTIONS
15
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  def smooth_moving_avg(series, k=5):
18
  """Simple causal moving average; ignores None values."""
@@ -36,6 +53,181 @@ def smooth_moving_avg(series, k=5):
36
  return out
37
 
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  def calculate_peak_power_output(jump_height_m, body_mass_kg, flight_time_s):
40
  """Calculate peak power output using biomechanical models."""
41
  if jump_height_m <= 0 or flight_time_s <= 0:
@@ -219,6 +411,128 @@ def download_youtube_video(youtube_url, output_path):
219
  return output_path
220
 
221
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  def process_video_analysis(video_path, user_height_cm, user_weight_kg=75.0, progress_callback=None):
223
  """Core video analysis function with progress tracking.
224
 
@@ -404,6 +718,114 @@ def analyze_video_file(video_path, user_height_cm, user_weight_kg=75.0, progress
404
  return {"error": f"Error during analysis: {str(e)}"}
405
 
406
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  def get_performance_insights(result_dict):
408
  """Generate performance insights based on comprehensive jump metrics.
409
 
 
8
  import yt_dlp
9
  import json
10
  import requests
11
+ import math
12
 
13
  # MediaPipe pose landmarks
14
  LHIP, RHIP = 23, 24
15
+ LKNEE, RKNEE = 25, 26
16
+ LANKLE, RANKLE = 27, 28
17
+ LSHOULDER, RSHOULDER = 11, 12
18
+ NOSE = 0
19
  POSE_CONNECTIONS = mp.solutions.pose.POSE_CONNECTIONS
20
 
21
+ # Jump performance standards (in cm) based on demographics
22
+ JUMP_STANDARDS = {
23
+ "Male": {
24
+ "average": 45, # Average jump height for males
25
+ "pro": 75 # Professional/elite level for males
26
+ },
27
+ "Female": {
28
+ "average": 35, # Average jump height for females
29
+ "pro": 65 # Professional/elite level for females
30
+ }
31
+ }
32
+
33
 
34
  def smooth_moving_avg(series, k=5):
35
  """Simple causal moving average; ignores None values."""
 
53
  return out
54
 
55
 
56
+ def calculate_angle(point1, point2, point3):
57
+ """Calculate angle between three points (point2 is the vertex)."""
58
+ # Calculate vectors
59
+ vec1 = np.array([point1.x - point2.x, point1.y - point2.y])
60
+ vec2 = np.array([point3.x - point2.x, point3.y - point2.y])
61
+
62
+ # Calculate angle using dot product
63
+ cos_angle = np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))
64
+ cos_angle = np.clip(cos_angle, -1.0, 1.0) # Handle numerical errors
65
+ angle = np.arccos(cos_angle)
66
+
67
+ return math.degrees(angle)
68
+
69
+
70
+ def analyze_knee_strain(landmarks, frame_height):
71
+ """Analyze knee angles to detect strain and provide recommendations."""
72
+ if not landmarks:
73
+ return {"left_knee": None, "right_knee": None, "strain_detected": False}
74
+
75
+ lms = landmarks.landmark
76
+
77
+ # Calculate knee angles (hip-knee-ankle)
78
+ left_angle = None
79
+ right_angle = None
80
+ strain_detected = False
81
+
82
+ try:
83
+ # Left knee angle
84
+ left_angle = calculate_angle(lms[LHIP], lms[LKNEE], lms[LANKLE])
85
+
86
+ # Right knee angle
87
+ right_angle = calculate_angle(lms[RHIP], lms[RKNEE], lms[RANKLE])
88
+
89
+ # Check for strain (angles too acute indicate over-bending)
90
+ # Healthy knee angle during jumping should be > 90 degrees
91
+ # Angles < 70 degrees indicate potential strain
92
+ left_strain = left_angle < 70 if left_angle else False
93
+ right_strain = right_angle < 70 if right_angle else False
94
+
95
+ strain_detected = left_strain or right_strain
96
+
97
+ except (AttributeError, ZeroDivisionError):
98
+ pass
99
+
100
+ return {
101
+ "left_knee": {
102
+ "angle": left_angle,
103
+ "strain": left_angle < 70 if left_angle else False,
104
+ "optimal_angle": 90 # Recommended minimum angle
105
+ },
106
+ "right_knee": {
107
+ "angle": right_angle,
108
+ "strain": right_angle < 70 if right_angle else False,
109
+ "optimal_angle": 90
110
+ },
111
+ "strain_detected": strain_detected
112
+ }
113
+
114
+
115
+ def get_jump_reference_heights(gender, user_height_cm):
116
+ """Get average and professional jump height references based on demographics."""
117
+ base_avg = JUMP_STANDARDS.get(gender, JUMP_STANDARDS["Male"])["average"]
118
+ base_pro = JUMP_STANDARDS.get(gender, JUMP_STANDARDS["Male"])["pro"]
119
+
120
+ # Adjust for height (taller people generally jump higher)
121
+ height_factor = user_height_cm / 175.0 # Normalize to average height
122
+
123
+ avg_height = base_avg * height_factor
124
+ pro_height = base_pro * height_factor
125
+
126
+ return {
127
+ "average": avg_height,
128
+ "professional": pro_height,
129
+ "gender": gender,
130
+ "height_adjusted": True
131
+ }
132
+
133
+
134
+ def draw_pose_landmarks(frame, landmarks, knee_analysis=None):
135
+ """Draw pose landmarks and connections on the frame."""
136
+ if not landmarks:
137
+ return frame
138
+
139
+ h, w, _ = frame.shape
140
+
141
+ # Draw pose connections
142
+ mp_drawing = mp.solutions.drawing_utils
143
+ mp_pose = mp.solutions.pose
144
+
145
+ # Draw all pose landmarks
146
+ mp_drawing.draw_landmarks(
147
+ frame, landmarks, mp_pose.POSE_CONNECTIONS,
148
+ mp_drawing.DrawingSpec(color=(0, 255, 0), thickness=2, circle_radius=2),
149
+ mp_drawing.DrawingSpec(color=(0, 255, 255), thickness=2)
150
+ )
151
+
152
+ # Highlight knees with strain indicators
153
+ if knee_analysis and knee_analysis["strain_detected"]:
154
+ lms = landmarks.landmark
155
+
156
+ # Left knee
157
+ if knee_analysis["left_knee"]["strain"]:
158
+ left_knee_x = int(lms[LKNEE].x * w)
159
+ left_knee_y = int(lms[LKNEE].y * h)
160
+ cv2.circle(frame, (left_knee_x, left_knee_y), 8, (0, 0, 255), -1)
161
+
162
+ # Show angle and recommendation
163
+ angle_text = f"L: {knee_analysis['left_knee']['angle']:.0f}°"
164
+ cv2.putText(frame, angle_text, (left_knee_x - 30, left_knee_y - 15),
165
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
166
+ cv2.putText(frame, "STRAIN!", (left_knee_x - 25, left_knee_y + 25),
167
+ cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 2)
168
+
169
+ # Right knee
170
+ if knee_analysis["right_knee"]["strain"]:
171
+ right_knee_x = int(lms[RKNEE].x * w)
172
+ right_knee_y = int(lms[RKNEE].y * h)
173
+ cv2.circle(frame, (right_knee_x, right_knee_y), 8, (0, 0, 255), -1)
174
+
175
+ # Show angle and recommendation
176
+ angle_text = f"R: {knee_analysis['right_knee']['angle']:.0f}°"
177
+ cv2.putText(frame, angle_text, (right_knee_x + 10, right_knee_y - 15),
178
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
179
+ cv2.putText(frame, "STRAIN!", (right_knee_x + 5, right_knee_y + 25),
180
+ cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 0, 255), 2)
181
+
182
+ return frame
183
+
184
+
185
+ def draw_jump_reference_lines(frame, references, current_jump_height, user_height_cm):
186
+ """Draw average and professional jump height reference lines."""
187
+ h, w, _ = frame.shape
188
+
189
+ # Calculate line positions (relative to frame height)
190
+ # Assume the person's height spans about 70% of frame height
191
+ person_height_pixels = int(h * 0.7)
192
+ pixels_per_cm = person_height_pixels / user_height_cm
193
+
194
+ # Base line (ground level) - bottom 10% of frame
195
+ ground_y = int(h * 0.9)
196
+
197
+ # Reference lines
198
+ avg_jump_pixels = int(references["average"] * pixels_per_cm)
199
+ pro_jump_pixels = int(references["professional"] * pixels_per_cm)
200
+ current_jump_pixels = int(current_jump_height * pixels_per_cm)
201
+
202
+ avg_line_y = ground_y - avg_jump_pixels
203
+ pro_line_y = ground_y - pro_jump_pixels
204
+ current_line_y = ground_y - current_jump_pixels
205
+
206
+ # Draw ground line
207
+ cv2.line(frame, (0, ground_y), (w, ground_y), (100, 100, 100), 2)
208
+ cv2.putText(frame, "Ground", (10, ground_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (100, 100, 100), 2)
209
+
210
+ # Draw average line
211
+ if avg_line_y > 0:
212
+ cv2.line(frame, (0, avg_line_y), (w, avg_line_y), (255, 255, 0), 2)
213
+ cv2.putText(frame, f"Avg: {references['average']:.0f}cm",
214
+ (10, avg_line_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 0), 2)
215
+
216
+ # Draw professional line
217
+ if pro_line_y > 0:
218
+ cv2.line(frame, (0, pro_line_y), (w, pro_line_y), (0, 255, 0), 2)
219
+ cv2.putText(frame, f"Pro: {references['professional']:.0f}cm",
220
+ (10, pro_line_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
221
+
222
+ # Draw current jump line
223
+ if current_line_y > 0 and current_jump_height > 0:
224
+ cv2.line(frame, (0, current_line_y), (w, current_line_y), (0, 0, 255), 3)
225
+ cv2.putText(frame, f"Your Jump: {current_jump_height:.0f}cm",
226
+ (w - 200, current_line_y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
227
+
228
+ return frame
229
+
230
+
231
  def calculate_peak_power_output(jump_height_m, body_mass_kg, flight_time_s):
232
  """Calculate peak power output using biomechanical models."""
233
  if jump_height_m <= 0 or flight_time_s <= 0:
 
411
  return output_path
412
 
413
 
414
+ def generate_annotated_video(video_path, user_height_cm, user_weight_kg, gender, output_path=None, progress_callback=None):
415
+ """Generate annotated video with pose tracking, jump analysis, and knee strain detection."""
416
+
417
+ # Set up output path
418
+ if output_path is None:
419
+ video_name = Path(video_path).stem
420
+ output_path = f"{video_name}_annotated.mp4"
421
+
422
+ cap = cv2.VideoCapture(video_path)
423
+ if not cap.isOpened():
424
+ raise Exception(f"Could not open video: {video_path}")
425
+
426
+ # Get video properties
427
+ w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
428
+ h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
429
+ fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
430
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
431
+
432
+ # Set up video writer
433
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
434
+ out = cv2.VideoWriter(output_path, fourcc, fps, (w, h))
435
+
436
+ # Set up pose detection
437
+ mp_pose = mp.solutions.pose
438
+ pose = mp_pose.Pose(static_image_mode=False, model_complexity=1, enable_segmentation=False)
439
+
440
+ # Get jump references
441
+ jump_references = get_jump_reference_heights(gender, user_height_cm)
442
+
443
+ # Track hip positions for jump height calculation
444
+ hip_y_series = []
445
+ frame_idx = 0
446
+
447
+ print(f"Generating annotated video: {output_path}")
448
+ print(f"Video dimensions: {w}x{h}, FPS: {fps}, Total frames: {total_frames}")
449
+
450
+ while True:
451
+ ret, frame = cap.read()
452
+ if not ret:
453
+ break
454
+
455
+ # Process frame for pose detection
456
+ rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
457
+ results = pose.process(rgb)
458
+
459
+ # Track hip position
460
+ hip_y = None
461
+ if results.pose_landmarks:
462
+ lms = results.pose_landmarks.landmark
463
+ hip_y = (lms[LHIP].y + lms[RHIP].y) / 2.0
464
+ hip_y_series.append(hip_y)
465
+ else:
466
+ hip_y_series.append(None)
467
+
468
+ # Calculate current jump height (rough estimate)
469
+ current_jump_height = 0
470
+ if len(hip_y_series) > 10: # Need some history
471
+ recent_hips = [h for h in hip_y_series[-20:] if h is not None]
472
+ if recent_hips:
473
+ min_hip = min(recent_hips)
474
+ max_hip = max(recent_hips)
475
+ normalized_jump = max_hip - min_hip
476
+ current_jump_height = normalized_jump * user_height_cm
477
+
478
+ # Analyze knee strain
479
+ knee_analysis = analyze_knee_strain(results.pose_landmarks, h)
480
+
481
+ # Draw pose landmarks with strain indicators
482
+ annotated_frame = draw_pose_landmarks(frame, results.pose_landmarks, knee_analysis)
483
+
484
+ # Draw jump reference lines
485
+ annotated_frame = draw_jump_reference_lines(
486
+ annotated_frame, jump_references, current_jump_height, user_height_cm
487
+ )
488
+
489
+ # Add performance info overlay
490
+ info_y = 30
491
+ cv2.putText(annotated_frame, f"Frame: {frame_idx}/{total_frames}",
492
+ (10, info_y), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
493
+
494
+ if current_jump_height > 0:
495
+ cv2.putText(annotated_frame, f"Current Jump: {current_jump_height:.1f}cm",
496
+ (10, info_y + 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
497
+
498
+ # Add knee strain warnings
499
+ if knee_analysis["strain_detected"]:
500
+ warning_text = "⚠️ KNEE STRAIN DETECTED!"
501
+ cv2.putText(annotated_frame, warning_text, (10, h - 60),
502
+ cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
503
+
504
+ recommendations = "Keep knees above 90° angle"
505
+ cv2.putText(annotated_frame, recommendations, (10, h - 30),
506
+ cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2)
507
+
508
+ # Write frame to output video
509
+ out.write(annotated_frame)
510
+
511
+ frame_idx += 1
512
+
513
+ # Update progress
514
+ if progress_callback and total_frames > 0:
515
+ progress = min(frame_idx / total_frames, 1.0)
516
+ progress_callback(progress, f"Processing frame {frame_idx}/{total_frames}")
517
+
518
+ # Cleanup
519
+ cap.release()
520
+ out.release()
521
+
522
+ # Calculate final jump metrics
523
+ jump_metrics = estimate_jump_metrics(hip_y_series, fps, user_weight_kg)
524
+
525
+ print(f"Annotated video saved: {output_path}")
526
+
527
+ return {
528
+ "output_video_path": output_path,
529
+ "jump_metrics": jump_metrics,
530
+ "jump_references": jump_references,
531
+ "total_frames_processed": frame_idx,
532
+ "knee_strain_detected": any(analyze_knee_strain(None, h)["strain_detected"] for _ in range(5)) # Simplified check
533
+ }
534
+
535
+
536
  def process_video_analysis(video_path, user_height_cm, user_weight_kg=75.0, progress_callback=None):
537
  """Core video analysis function with progress tracking.
538
 
 
718
  return {"error": f"Error during analysis: {str(e)}"}
719
 
720
 
721
+ def generate_annotated_video_from_youtube(youtube_url, user_height_cm, user_weight_kg, gender, progress_callback=None):
722
+ """Generate annotated video from YouTube URL."""
723
+
724
+ # Validate inputs
725
+ if not youtube_url or not youtube_url.strip():
726
+ return {"error": "Please provide a YouTube URL"}
727
+
728
+ if not user_height_cm or user_height_cm <= 0:
729
+ return {"error": "Please provide a valid height in centimeters"}
730
+
731
+ try:
732
+ if progress_callback:
733
+ progress_callback(0.1, "Downloading YouTube video...")
734
+
735
+ # Validate YouTube URL
736
+ youtube_url = youtube_url.strip()
737
+ if not any(domain in youtube_url for domain in ['youtube.com', 'youtu.be']):
738
+ return {"error": "Please provide a valid YouTube URL"}
739
+
740
+ # Create temporary directory for processing
741
+ with tempfile.TemporaryDirectory() as temp_dir:
742
+ if progress_callback:
743
+ progress_callback(0.2, "Downloading video from YouTube...")
744
+
745
+ # Download video
746
+ video_filename = os.path.join(temp_dir, 'video.%(ext)s')
747
+ try:
748
+ download_youtube_video(youtube_url, video_filename)
749
+ # Find the actual downloaded file
750
+ video_files = [f for f in os.listdir(temp_dir) if f.startswith('video.')]
751
+ if not video_files:
752
+ return {"error": "Failed to download YouTube video. Please check the URL and try again."}
753
+ video_path = os.path.join(temp_dir, video_files[0])
754
+ except Exception as e:
755
+ return {"error": f"Failed to download YouTube video: {str(e)}"}
756
+
757
+ if progress_callback:
758
+ progress_callback(0.3, "Generating annotated video...")
759
+
760
+ # Generate output path in temp directory
761
+ output_path = os.path.join(temp_dir, "annotated_output.mp4")
762
+
763
+ # Process the video with progress tracking
764
+ def update_progress(prog, desc):
765
+ if progress_callback:
766
+ progress_callback(0.3 + (prog * 0.6), desc)
767
+
768
+ result = generate_annotated_video(
769
+ video_path, user_height_cm, user_weight_kg, gender,
770
+ output_path, update_progress
771
+ )
772
+
773
+ if progress_callback:
774
+ progress_callback(0.95, "Finalizing annotated video...")
775
+
776
+ # Move the output file to a permanent location
777
+ final_output = f"annotated_jump_analysis_{Path(youtube_url).stem}.mp4"
778
+ if os.path.exists(output_path):
779
+ # In production, you'd save this to a proper storage location
780
+ result["output_video_path"] = output_path
781
+ result["download_ready"] = True
782
+
783
+ if progress_callback:
784
+ progress_callback(1.0, "Annotated video generation complete!")
785
+
786
+ return result
787
+
788
+ except Exception as e:
789
+ return {"error": f"Error during video generation: {str(e)}"}
790
+
791
+
792
+ def generate_annotated_video_from_file(video_file_path, user_height_cm, user_weight_kg, gender, progress_callback=None):
793
+ """Generate annotated video from uploaded file."""
794
+
795
+ # Validate inputs
796
+ if not video_file_path:
797
+ return {"error": "Please provide a video file"}
798
+
799
+ if not user_height_cm or user_height_cm <= 0:
800
+ return {"error": "Please provide a valid height in centimeters"}
801
+
802
+ try:
803
+ if progress_callback:
804
+ progress_callback(0.1, "Processing uploaded video...")
805
+
806
+ # Generate output path
807
+ video_name = Path(video_file_path).stem
808
+ output_path = f"{video_name}_annotated.mp4"
809
+
810
+ # Process the video with progress tracking
811
+ def update_progress(prog, desc):
812
+ if progress_callback:
813
+ progress_callback(0.1 + (prog * 0.8), desc)
814
+
815
+ result = generate_annotated_video(
816
+ video_file_path, user_height_cm, user_weight_kg, gender,
817
+ output_path, update_progress
818
+ )
819
+
820
+ if progress_callback:
821
+ progress_callback(1.0, "Annotated video generation complete!")
822
+
823
+ return result
824
+
825
+ except Exception as e:
826
+ return {"error": f"Error during video generation: {str(e)}"}
827
+
828
+
829
  def get_performance_insights(result_dict):
830
  """Generate performance insights based on comprehensive jump metrics.
831