chenemii commited on
Commit
dd3f6cd
·
2 Parent(s): a26cc05 af7ef4f

git push origin mainMerge remote-tracking branch 'github/main'

Browse files
app/main.py CHANGED
@@ -13,7 +13,7 @@ load_dotenv()
13
  # Add the app directory to the path
14
  sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15
 
16
- from app.utils.video_downloader import download_youtube_video
17
  from app.utils.video_processor import process_video
18
  from app.models.pose_estimator import analyze_pose
19
  from app.models.swing_analyzer import segment_swing, analyze_trajectory
@@ -33,11 +33,12 @@ def main():
33
  "\nEnable GPT analysis? (y/n, default: y): ").lower() != 'n'
34
 
35
  sample_rate_input = input(
36
- "\nFrame skip rate for YOLO (1-10, default: 5, auto-adjusts for videos shorter than 5 seconds): ")
37
- sample_rate = 5 # Default value
38
  if sample_rate_input.isdigit():
39
  sample_rate = max(1, min(10, int(sample_rate_input)))
40
 
 
41
  try:
42
  # Step 3: Download the video
43
  print("\nDownloading video...")
@@ -95,7 +96,11 @@ def main():
95
 
96
  except Exception as e:
97
  print(f"\nError: {str(e)}")
98
- return
 
 
 
 
99
 
100
 
101
  if __name__ == "__main__":
 
13
  # Add the app directory to the path
14
  sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15
 
16
+ from app.utils.video_downloader import download_youtube_video, cleanup_video_file
17
  from app.utils.video_processor import process_video
18
  from app.models.pose_estimator import analyze_pose
19
  from app.models.swing_analyzer import segment_swing, analyze_trajectory
 
33
  "\nEnable GPT analysis? (y/n, default: y): ").lower() != 'n'
34
 
35
  sample_rate_input = input(
36
+ "\nFrame processing rate for YOLO (1-10, default: 1 for all frames): ")
37
+ sample_rate = 1 # Default value - process all frames
38
  if sample_rate_input.isdigit():
39
  sample_rate = max(1, min(10, int(sample_rate_input)))
40
 
41
+ video_path = None # Initialize video_path for cleanup
42
  try:
43
  # Step 3: Download the video
44
  print("\nDownloading video...")
 
96
 
97
  except Exception as e:
98
  print(f"\nError: {str(e)}")
99
+ finally:
100
+ # Clean up the original downloaded video file after processing
101
+ if video_path:
102
+ print("\nCleaning up downloaded video file...")
103
+ cleanup_video_file(video_path)
104
 
105
 
106
  if __name__ == "__main__":
app/models/llm_analyzer.py CHANGED
@@ -6,6 +6,9 @@ import json
6
  import httpx
7
  from openai import OpenAI
8
  import streamlit as st
 
 
 
9
 
10
 
11
  def check_llm_services():
@@ -61,15 +64,14 @@ def generate_swing_analysis(pose_data, swing_phases, trajectory_data):
61
  trajectory_data (dict): Dictionary mapping frame indices to trajectory data
62
 
63
  Returns:
64
- str: Detailed swing analysis and coaching tips
65
  """
66
  # Check available services
67
  services = check_llm_services()
68
 
69
- # If no services are available, return sample analysis
70
- if not services['ollama']['available'] and not services['openai'][
71
- 'available']:
72
- return get_sample_analysis()
73
 
74
  # Prepare data for LLM
75
  analysis_data = prepare_data_for_llm(pose_data, swing_phases,
@@ -84,7 +86,7 @@ def generate_swing_analysis(pose_data, swing_phases, trajectory_data):
84
  if analysis:
85
  return analysis
86
  except Exception as e:
87
- print(f"Error with Ollama: {str(e)}. Falling back to OpenAI...")
88
 
89
  # Try OpenAI if available
90
  if services['openai']['available']:
@@ -94,11 +96,10 @@ def generate_swing_analysis(pose_data, swing_phases, trajectory_data):
94
  if analysis:
95
  return analysis
96
  except Exception as e:
97
- print(
98
- f"Error with OpenAI: {str(e)}. Using sample analysis instead.")
99
 
100
- # If both services failed, return sample analysis
101
- return get_sample_analysis()
102
 
103
 
104
  def call_ollama_service(prompt, config):
@@ -164,7 +165,7 @@ def call_openai_service(prompt, config):
164
  try:
165
  # Try with GPT-4 first
166
  response = client.chat.completions.create(
167
- model="gpt-4-turbo",
168
  messages=[{
169
  "role":
170
  "system",
@@ -211,304 +212,1115 @@ def call_openai_service(prompt, config):
211
  return None
212
 
213
 
214
- def get_sample_analysis():
215
- """
216
- Return sample analysis when no LLM services are available
217
-
218
- Returns:
219
- str: Sample swing analysis
220
- """
221
- return """
222
- ## Swing Analysis Summary
223
-
224
- Based on the video analysis, here are some observations about your swing:
225
-
226
- ### Setup Phase
227
- - Your stance appears slightly wider than shoulder-width, which can provide good stability
228
- - Your posture shows a good spine angle, though you could bend slightly more from the hips
229
- - The ball position looks appropriate for the club you're using
230
-
231
- ### Backswing
232
- - Your takeaway is smooth with good tempo
233
- - Your wrist hinge develops appropriately in the backswing
234
- - Your right elbow could be kept a bit closer to your body for better consistency
235
-
236
- ### Downswing
237
- - Good weight transfer from back foot to front foot during the transition
238
- - Your hips are rotating well through impact
239
- - The swing plane looks consistent throughout the downswing
240
-
241
- ### Impact
242
- - Club face alignment at impact appears slightly open
243
- - Your head position is stable through impact
244
- - The club path is on a good line toward the target
245
-
246
- ### Follow Through
247
- - Good balance maintained through the finish
248
- - Full extension of arms after impact
249
- - Complete rotation of the body toward the target
250
-
251
- ## Areas for Improvement
252
-
253
- 1. **Club Face Control**: The slightly open club face at impact suggests you may be prone to slicing the ball. Focus on maintaining a square club face through impact.
254
-
255
- 2. **Right Elbow Position**: Keeping your right elbow closer to your body during the backswing will help create a more consistent swing plane.
256
-
257
- 3. **Hip Rotation**: While your hip rotation is good, increasing the speed of rotation could generate more power in your swing.
258
-
259
- 4. **Wrist Release**: Your wrist release could be more active through impact to generate additional club head speed.
260
-
261
- These adjustments should help improve both consistency and distance in your swing.
262
- """
263
-
264
-
265
- def prepare_data_for_llm(pose_data, swing_phases, trajectory_data):
266
  """
267
  Prepare swing data for LLM analysis
268
 
269
  Args:
270
  pose_data (dict): Dictionary mapping frame indices to pose keypoints
271
  swing_phases (dict): Dictionary mapping phase names to lists of frame indices
272
- trajectory_data (dict): Dictionary mapping frame indices to trajectory data
273
 
274
  Returns:
275
- dict: Processed data for LLM analysis
276
  """
277
- analysis_data = {"swing_phases": {}, "joint_angles": {}, "trajectory": {}}
278
-
279
- # Process swing phases
280
- for phase, frames in swing_phases.items():
281
- if frames:
282
- # Get a representative frame for each phase
283
- mid_frame = frames[len(frames) // 2]
284
-
285
- # Get joint angles for the representative frame
286
- if mid_frame in pose_data:
287
- keypoints = pose_data[mid_frame]
288
-
289
- # Calculate key metrics for each phase
290
- analysis_data["swing_phases"][phase] = {
291
- "frame_index": mid_frame,
292
- "duration_frames": len(frames)
293
- }
294
-
295
- # Process trajectory data
296
- impact_frames = swing_phases.get("impact", [])
297
- if impact_frames:
298
- impact_frame = impact_frames[len(impact_frames) // 2]
299
- if impact_frame in trajectory_data:
300
- impact_data = trajectory_data[impact_frame]
301
- if "club_speed" in impact_data and impact_data["club_speed"]:
302
- analysis_data["trajectory"]["club_speed_mph"] = impact_data[
303
- "club_speed"]
304
-
305
- # Calculate backswing and downswing durations if available
306
- backswing_frames = swing_phases.get("backswing", [])
307
  downswing_frames = swing_phases.get("downswing", [])
308
-
309
- backswing_duration = None
310
- downswing_duration = None
311
-
312
- if backswing_frames:
313
- # Assuming 30 fps video
314
- backswing_duration = len(backswing_frames) / 30.0
315
-
316
- if downswing_frames:
317
- # Assuming 30 fps video
318
- downswing_duration = len(downswing_frames) / 30.0
319
-
320
- # Calculate tempo ratio if both durations are available
321
- tempo_ratio = None
322
- if backswing_duration and downswing_duration and downswing_duration > 0:
323
- tempo_ratio = backswing_duration / downswing_duration
324
-
325
- # Add comprehensive metrics with default values or calculated values
326
- # These values would normally be calculated from pose and trajectory data
327
- analysis_data["metrics"] = {
328
- # Core body mechanics
329
- "tempo_ratio": tempo_ratio or 3.0, # Backswing to downswing time ratio
330
- "swing_plane_consistency": 0.85, # 0-1 scale
331
- "weight_shift": 0.7, # 0-1 scale
332
- "hip_rotation": 45, # degrees
333
- "shoulder_rotation": 90, # degrees
334
- "posture_score": 0.8, # 0-1 scale
335
-
336
- # Upper body mechanics
337
- "arm_extension": 0.8, # 0-1 scale
338
- "wrist_hinge": 80, # degrees
339
- "chest_rotation_efficiency": 0.75, # 0-1 scale
340
- "head_movement_lateral": 2.5, # inches
341
- "head_movement_vertical": 1.8, # inches
342
-
343
- # Lower body mechanics
344
- "knee_flexion_address": 25, # degrees
345
- "knee_flexion_impact": 30, # degrees
346
- "hip_thrust": 0.6, # 0-1 scale
347
- "ground_force_efficiency": 0.7, # 0-1 scale
348
-
349
- # Club path and face metrics
350
- "swing_path":
351
- 2.5, # degrees (positive = out-to-in, negative = in-to-out)
352
- "clubface_angle": 2.1, # degrees (positive = open, negative = closed)
353
- "attack_angle":
354
- -4.2, # degrees (negative = descending, positive = ascending)
355
- "club_path_consistency": 0.78, # 0-1 scale
356
-
357
- # Tempo and timing metrics
358
- "transition_smoothness": 0.75, # 0-1 scale
359
- "backswing_duration": backswing_duration or 0.9, # seconds
360
- "downswing_duration": downswing_duration or 0.3, # seconds
361
- "kinematic_sequence": 0.82, # 0-1 scale
362
-
363
- # Efficiency and power metrics
364
- "energy_transfer": 0.78, # 0-1 scale
365
- "potential_distance": 240, # yards
366
- "power_accumulation": 0.75, # 0-1 scale
367
- "speed_generation": "Arms-dominant" # String description
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  }
369
-
370
- return analysis_data
371
 
372
 
373
  def create_llm_prompt(analysis_data):
374
  """
375
- Create a prompt for the LLM based on swing analysis data
376
 
377
  Args:
378
- analysis_data (dict): Processed swing analysis data
379
 
380
  Returns:
381
- str: Prompt for LLM
382
  """
383
- prompt = """
384
- I've analyzed a golf swing and extracted the following data:
385
-
386
- ## Swing Phases
387
- """
388
-
389
- # Add swing phases information
390
- for phase, data in analysis_data["swing_phases"].items():
391
- prompt += f"- {phase.capitalize()}: Frame {data['frame_index']}, Duration: {data['duration_frames']} frames\n"
392
-
393
- # Add trajectory information
394
- prompt += "\n## Trajectory Data\n"
395
- if "trajectory" in analysis_data and "club_speed_mph" in analysis_data[
396
- "trajectory"]:
397
- prompt += f"- Club Speed: {analysis_data['trajectory']['club_speed_mph']:.1f} mph\n"
398
-
399
- # Add detailed biomechanical metrics
400
- prompt += "\n## Swing Mechanics\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
 
402
  # Core body mechanics
403
- prompt += "\n### Body Mechanics\n"
404
- prompt += "- Tempo Ratio (Backswing:Downswing): {:.1f}\n".format(
405
- analysis_data["metrics"].get("tempo_ratio", 0))
406
- prompt += "- Hip Rotation (degrees): {}\n".format(
407
- analysis_data["metrics"].get("hip_rotation", 0))
408
- prompt += "- Shoulder Rotation (degrees): {}\n".format(
409
- analysis_data["metrics"].get("shoulder_rotation", 0))
410
- prompt += "- Posture Score: {}%\n".format(
411
- int(analysis_data["metrics"].get("posture_score", 0) * 100))
412
 
413
  # Upper body mechanics
414
  prompt += "\n### Upper Body Mechanics\n"
415
- prompt += "- Arm Extension (impact): {}%\n".format(
416
- int(analysis_data["metrics"].get("arm_extension", 0.8) * 100))
417
- prompt += "- Wrist Hinge (degrees): {}\n".format(
418
- analysis_data["metrics"].get("wrist_hinge", 0))
419
- prompt += "- Shoulder Plane Consistency: {}%\n".format(
420
- int(analysis_data["metrics"].get("swing_plane_consistency", 0) * 100))
421
- prompt += "- Chest Rotation Efficiency: {}%\n".format(
422
- int(analysis_data["metrics"].get("chest_rotation_efficiency", 0.75) *
423
- 100))
424
- prompt += "- Head Movement (lateral): {}in\n".format(
425
- analysis_data["metrics"].get("head_movement_lateral", 2.5))
426
- prompt += "- Head Movement (vertical): {}in\n".format(
427
- analysis_data["metrics"].get("head_movement_vertical", 1.8))
428
 
429
  # Lower body mechanics
430
  prompt += "\n### Lower Body Mechanics\n"
431
- prompt += "- Weight Shift (lead foot at impact): {}%\n".format(
432
- int(analysis_data["metrics"].get("weight_shift", 0) * 100))
433
- prompt += "- Knee Flexion (address): {}°\n".format(
434
- analysis_data["metrics"].get("knee_flexion_address", 25))
435
- prompt += "- Knee Flexion (impact): {}°\n".format(
436
- analysis_data["metrics"].get("knee_flexion_impact", 30))
437
- prompt += "- Hip Thrust (impact): {}%\n".format(
438
- int(analysis_data["metrics"].get("hip_thrust", 0.6) * 100))
439
- prompt += "- Ground Force Efficiency: {}%\n".format(
440
- int(analysis_data["metrics"].get("ground_force_efficiency", 0.7) *
441
- 100))
442
-
443
- # Swing path and clubface metrics
444
- prompt += "\n### Club Path & Face Metrics\n"
445
- prompt += "- Swing Path (degrees): {} ({})\n".format(
446
- analysis_data["metrics"].get("swing_path", 2.5), "Out-to-In"
447
- if analysis_data["metrics"].get("swing_path", 0) > 0 else "In-to-Out")
448
- prompt += "- Clubface Angle (degrees): {} ({})\n".format(
449
- analysis_data["metrics"].get("clubface_angle", 2.1), "Open"
450
- if analysis_data["metrics"].get("clubface_angle", 0) > 0 else "Closed")
451
- prompt += "- Attack Angle (degrees): {} ({})\n".format(
452
- analysis_data["metrics"].get("attack_angle", -4.2), "Descending" if
453
- analysis_data["metrics"].get("attack_angle", 0) < 0 else "Ascending")
454
- prompt += "- Club Path Consistency: {}%\n".format(
455
- int(analysis_data["metrics"].get("club_path_consistency", 0.78) * 100))
456
-
457
- # Tempo and timing metrics
458
- prompt += "\n### Tempo & Timing\n"
459
- prompt += "- Transition Smoothness: {}%\n".format(
460
- int(analysis_data["metrics"].get("transition_smoothness", 0.75) * 100))
461
- prompt += "- Backswing Duration: {} seconds\n".format(
462
- analysis_data["metrics"].get("backswing_duration", 0.9))
463
- prompt += "- Downswing Duration: {} seconds\n".format(
464
- analysis_data["metrics"].get("downswing_duration", 0.3))
465
- prompt += "- Sequential Kinematic Sequence: {}%\n".format(
466
- int(analysis_data["metrics"].get("kinematic_sequence", 0.82) * 100))
467
 
468
  # Efficiency and power metrics
469
  prompt += "\n### Efficiency & Power Metrics\n"
470
- prompt += "- Energy Transfer Efficiency: {}%\n".format(
471
- int(analysis_data["metrics"].get("energy_transfer", 0.78) * 100))
472
- prompt += "- Potential Distance: {} yards\n".format(
473
- analysis_data["metrics"].get("potential_distance", 240))
474
- prompt += "- Power Accumulation: {}%\n".format(
475
- int(analysis_data["metrics"].get("power_accumulation", 0.75) * 100))
476
- prompt += "- Speed Generation Method: {}\n".format(
477
- analysis_data["metrics"].get("speed_generation", "Arms-dominant"))
478
 
479
  prompt += """
480
 
481
- Based on this detailed biomechanical data, please provide:
482
-
483
- 1. A comprehensive analysis of the golf swing including:
484
- - Detailed breakdown of each swing phase
485
- - Analysis of body mechanics and kinematic sequence
486
- - Assessment of power generation and efficiency
487
- - Evaluation of clubface control and swing path
488
-
489
- 2. Key strengths and weaknesses in the swing, including:
490
- - Specific biomechanical inefficiencies
491
- - Compensatory movements
492
- - Physical limitations
493
- - Technical flaws
494
-
495
- 3. Prioritized recommendations for improvement:
496
- - Top 3-5 most impactful changes to make
497
- - Root cause analysis (why these issues are occurring)
498
- - Expected improvement in performance from each change
499
-
500
- 4. Specific drills and exercises addressing each issue:
501
- - Technical drills for swing mechanics
502
- - Physical exercises to address any biomechanical limitations
503
- - Feel-based drills to develop proper movement patterns
504
- - Practice routine recommendations
505
-
506
- 5. Long-term development plan:
507
- - Sequential order of what to work on
508
- - Benchmarks for measuring progress
509
- - Timeline for improvement
510
-
511
- Please be specific, detailed, and actionable in your feedback, providing the kind of analysis a professional golf coach would give after a thorough assessment.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
512
  """
513
 
514
  return prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  import httpx
7
  from openai import OpenAI
8
  import streamlit as st
9
+ import re
10
+ import numpy as np
11
+ from app.models.pose_estimator import calculate_joint_angles
12
 
13
 
14
  def check_llm_services():
 
64
  trajectory_data (dict): Dictionary mapping frame indices to trajectory data
65
 
66
  Returns:
67
+ str: Detailed swing analysis and coaching tips, or error message
68
  """
69
  # Check available services
70
  services = check_llm_services()
71
 
72
+ # If no services are available, return error message
73
+ if not services['ollama']['available'] and not services['openai']['available']:
74
+ return "Error: No AI services available. Please ensure either Ollama is running or OpenAI API key is configured."
 
75
 
76
  # Prepare data for LLM
77
  analysis_data = prepare_data_for_llm(pose_data, swing_phases,
 
86
  if analysis:
87
  return analysis
88
  except Exception as e:
89
+ print(f"Error with Ollama: {str(e)}")
90
 
91
  # Try OpenAI if available
92
  if services['openai']['available']:
 
96
  if analysis:
97
  return analysis
98
  except Exception as e:
99
+ print(f"Error with OpenAI: {str(e)}")
 
100
 
101
+ # If both services failed, return error message
102
+ return "Error: All AI services failed. Please check your API keys and service configurations."
103
 
104
 
105
  def call_ollama_service(prompt, config):
 
165
  try:
166
  # Try with GPT-4 first
167
  response = client.chat.completions.create(
168
+ model="gpt-4o-mini",
169
  messages=[{
170
  "role":
171
  "system",
 
212
  return None
213
 
214
 
215
+ def prepare_data_for_llm(pose_data, swing_phases, trajectory_data=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  """
217
  Prepare swing data for LLM analysis
218
 
219
  Args:
220
  pose_data (dict): Dictionary mapping frame indices to pose keypoints
221
  swing_phases (dict): Dictionary mapping phase names to lists of frame indices
222
+ trajectory_data (dict, optional): Ball trajectory data
223
 
224
  Returns:
225
+ dict: Formatted swing data for LLM
226
  """
227
+
228
+ # Calculate actual biomechanical metrics from pose data
229
+ bio_metrics = calculate_biomechanical_metrics(pose_data, swing_phases)
230
+
231
+ # Calculate phase durations and timing metrics
232
+ setup_frames = swing_phases.get("setup", [])
233
+ backswing_frames = swing_phases.get("backswing", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  downswing_frames = swing_phases.get("downswing", [])
235
+ impact_frames = swing_phases.get("impact", [])
236
+ follow_through_frames = swing_phases.get("follow_through", [])
237
+
238
+ # Calculate tempo ratio (downswing:backswing)
239
+ backswing_duration = len(backswing_frames) if backswing_frames else 1
240
+ downswing_duration = len(downswing_frames) if downswing_frames else 1
241
+ tempo_ratio = downswing_duration / backswing_duration if backswing_duration > 0 else 1.0
242
+
243
+ # Calculate total swing duration and club speed estimates
244
+ total_frames = len(setup_frames) + len(backswing_frames) + len(downswing_frames) + len(impact_frames) + len(follow_through_frames)
245
+
246
+ # Estimate club speed based on downswing duration (faster downswing = higher speed)
247
+ # Professional downswings are typically 10-15 frames at 30fps
248
+ if downswing_duration > 0:
249
+ speed_factor = max(0.5, min(2.0, 12.0 / downswing_duration)) # Normalize around 12 frames
250
+ estimated_club_speed = 70 + (speed_factor * 40) # Base 70 mph, up to 110 mph
251
+ else:
252
+ estimated_club_speed = 85
253
+
254
+ # Process joint angles if available
255
+ joint_angles = {}
256
+ if pose_data:
257
+ # Get a representative frame for joint analysis
258
+ rep_frame = None
259
+ if impact_frames:
260
+ rep_frame = impact_frames[0]
261
+ elif downswing_frames:
262
+ rep_frame = downswing_frames[len(downswing_frames) // 2]
263
+ elif backswing_frames:
264
+ rep_frame = backswing_frames[-1]
265
+
266
+ if rep_frame and rep_frame in pose_data:
267
+ try:
268
+ from app.models.pose_estimator import calculate_joint_angles
269
+ joint_angles = calculate_joint_angles(pose_data[rep_frame])
270
+ except Exception as e:
271
+ print(f"Error calculating joint angles: {e}")
272
+ joint_angles = {}
273
+
274
+ # Prepare the structured data
275
+ swing_data = {
276
+ "swing_phases": {
277
+ "setup": {
278
+ "frame_count": len(setup_frames),
279
+ "duration_ms": len(setup_frames) * 33.33 # Assuming 30fps
280
+ },
281
+ "backswing": {
282
+ "frame_count": len(backswing_frames),
283
+ "duration_ms": len(backswing_frames) * 33.33
284
+ },
285
+ "downswing": {
286
+ "frame_count": len(downswing_frames),
287
+ "duration_ms": len(downswing_frames) * 33.33
288
+ },
289
+ "impact": {
290
+ "frame_count": len(impact_frames),
291
+ "duration_ms": len(impact_frames) * 33.33
292
+ },
293
+ "follow_through": {
294
+ "frame_count": len(follow_through_frames),
295
+ "duration_ms": len(follow_through_frames) * 33.33
296
+ }
297
+ },
298
+
299
+ "timing_metrics": {
300
+ "tempo_ratio": round(tempo_ratio, 2),
301
+ "total_swing_frames": total_frames,
302
+ "total_swing_time_ms": total_frames * 33.33,
303
+ "estimated_club_speed_mph": round(estimated_club_speed, 1)
304
+ },
305
+
306
+ "biomechanical_metrics": {
307
+ # Core rotation metrics
308
+ "hip_rotation_degrees": round(bio_metrics.get("hip_rotation", 25), 1),
309
+ "shoulder_rotation_degrees": round(bio_metrics.get("shoulder_rotation", 60), 1),
310
+ "chest_rotation_efficiency_percent": round(bio_metrics.get("chest_rotation_efficiency", 0.6) * 100, 1),
311
+
312
+ # Weight transfer and stability
313
+ "weight_shift_percent": round(bio_metrics.get("weight_shift", 0.5) * 100, 1),
314
+ "ground_force_efficiency_percent": round(bio_metrics.get("ground_force_efficiency", 0.6) * 100, 1),
315
+ "hip_thrust_percent": round(bio_metrics.get("hip_thrust", 0.5) * 100, 1),
316
+
317
+ # Arm and club mechanics
318
+ "arm_extension_percent": round(bio_metrics.get("arm_extension", 0.6) * 100, 1),
319
+ "wrist_hinge_degrees": round(bio_metrics.get("wrist_hinge", 60), 1),
320
+ "swing_plane_consistency_percent": round(bio_metrics.get("swing_plane_consistency", 0.6) * 100, 1),
321
+
322
+ # Posture and stability
323
+ "posture_score_percent": round(bio_metrics.get("posture_score", 0.6) * 100, 1),
324
+ "head_movement_lateral_inches": round(bio_metrics.get("head_movement_lateral", 3.0), 1),
325
+ "head_movement_vertical_inches": round(bio_metrics.get("head_movement_vertical", 2.0), 1),
326
+
327
+ # Leg mechanics
328
+ "knee_flexion_address_degrees": round(bio_metrics.get("knee_flexion_address", 25), 1),
329
+ "knee_flexion_impact_degrees": round(bio_metrics.get("knee_flexion_impact", 30), 1),
330
+
331
+ # Advanced coordination metrics
332
+ "transition_smoothness_percent": round(bio_metrics.get("transition_smoothness", 0.6) * 100, 1),
333
+ "kinematic_sequence_percent": round(bio_metrics.get("kinematic_sequence", 0.6) * 100, 1),
334
+ "energy_transfer_efficiency_percent": round(bio_metrics.get("energy_transfer", 0.6) * 100, 1),
335
+ "power_accumulation_percent": round(bio_metrics.get("power_accumulation", 0.6) * 100, 1),
336
+
337
+ # Performance estimates
338
+ "potential_distance_yards": round(bio_metrics.get("potential_distance", 200), 0),
339
+ "speed_generation_method": bio_metrics.get("speed_generation", "Mixed")
340
+ },
341
+
342
+ "joint_angles": joint_angles,
343
+
344
+ "trajectory_analysis": trajectory_data if trajectory_data else {
345
+ "estimated_carry_distance": round(bio_metrics.get("potential_distance", 200) * 0.85, 0),
346
+ "estimated_ball_speed": round(estimated_club_speed * 1.4, 1), # Rough conversion
347
+ "trajectory_type": "Mid" if bio_metrics.get("arm_extension", 0.6) > 0.7 else "Low"
348
+ }
349
  }
350
+
351
+ return swing_data
352
 
353
 
354
  def create_llm_prompt(analysis_data):
355
  """
356
+ Create a comprehensive prompt for LLM analysis with professional benchmarks
357
 
358
  Args:
359
+ analysis_data (dict): Processed swing analysis data with biomechanical metrics
360
 
361
  Returns:
362
+ str: Formatted prompt for LLM analysis
363
  """
364
+
365
+ # Extract metrics from the new data structure
366
+ bio_metrics = analysis_data.get("biomechanical_metrics", {})
367
+ timing_metrics = analysis_data.get("timing_metrics", {})
368
+ swing_phases = analysis_data.get("swing_phases", {})
369
+
370
+ prompt = """# Golf Swing Analysis
371
+
372
+ ## PROFESSIONAL BENCHMARKS FOR CALIBRATION
373
+ Use these professional standards as your 100% reference for scoring. These represent elite-level golf swing mechanics based on actual LPGA Tour professional analysis:
374
+
375
+ ### Professional Golfer Analysis Summary (100% Reference Standards):
376
+
377
+ **Atthaya Thitikul (LPGA Tour - Elite Level):**
378
+ - Hip Rotation: 63.4°, Shoulder Rotation: 120°, Posture Score: 98.2%
379
+ - Weight Shift: 88.4%, Arm Extension: 99.8%, Wrist Hinge: 120°
380
+ - Energy Transfer: 96.1%, Power Accumulation: 100%, Potential Distance: 295 yards
381
+ - Sequential Kinematic Sequence: 100%, Swing Plane Consistency: 85%
382
+
383
+ **Nelly Korda (LPGA Tour - Elite Level):**
384
+ - Hip Rotation: 90°, Shoulder Rotation: 120°, Posture Score: 97.4%
385
+ - Weight Shift: 73.5%, Arm Extension: 96.7%, Wrist Hinge: 114.8°
386
+ - Energy Transfer: 91.2%, Power Accumulation: 100%, Potential Distance: 289 yards
387
+ - Sequential Kinematic Sequence: 100%, Swing Plane Consistency: 85%
388
+
389
+ **Demi Runas (Professional Level):**
390
+ - Hip Rotation: 63.4°, Shoulder Rotation: 120°, Posture Score: 95.9%
391
+ - Weight Shift: 63.9%, Arm Extension: 96.6%, Wrist Hinge: 93.4°
392
+ - Energy Transfer: 88.0%, Power Accumulation: 100%, Potential Distance: 286 yards
393
+ - Sequential Kinematic Sequence: 100%, Swing Plane Consistency: 85%
394
+
395
+ ### **PROFESSIONAL STANDARDS CALIBRATION (100% Level):**
396
+ **Core Biomechanical Metrics:**
397
+ - **Hip Rotation**: 60-90° (Exceptional body turn and flexibility)
398
+ - **Shoulder Rotation**: 120° (Full shoulder coil for maximum power)
399
+ - **Posture Score**: 95-98% (Exceptional spine angle consistency)
400
+ - **Weight Shift**: 70-88% (Excellent weight transfer to lead side)
401
+
402
+ **Upper Body Excellence:**
403
+ - **Arm Extension**: 96-100% (Near-perfect extension at impact)
404
+ - **Wrist Hinge**: 95-120° (Optimal lag and release timing)
405
+ - **Swing Plane Consistency**: 85% (Tour-level repeatability)
406
+ - **Chest Rotation Efficiency**: 100% (Perfect coordination)
407
+
408
+ **Power & Efficiency Markers:**
409
+ - **Energy Transfer Efficiency**: 88-96% (Elite power transfer)
410
+ - **Power Accumulation**: 100% (Maximum power generation)
411
+ - **Sequential Kinematic Sequence**: 100% (Perfect body sequencing)
412
+ - **Potential Distance**: 285-295 yards (Tour-level power)
413
+
414
+ **Movement Quality Standards:**
415
+ - **Head Movement**: 2-8 inches (Controlled, minimal excessive movement)
416
+ - **Ground Force Efficiency**: 70-88% (Excellent ground interaction)
417
+ - **Hip Thrust**: 40-100% (Strong lower body drive)
418
+
419
+ ### **AMATEUR REFERENCE EXAMPLES FOR CALIBRATION:**
420
+
421
+ **70% Level Skilled Amateur (Female):**
422
+ - Hip Rotation: 23.0°, Shoulder Rotation: 120° (Excellent shoulder turn, limited hip mobility)
423
+ - Posture Score: 89.5%, Weight Shift: 90.0% (Strong fundamentals)
424
+ - Arm Extension: 99.8%, Wrist Hinge: 49.4° (Great extension, needs more lag)
425
+ - Energy Transfer: 94.5%, Power Accumulation: 82.1% (Very good coordination)
426
+ - Potential Distance: 273 yards, Sequential Kinematic: 93.6%
427
+ - Head Movement: 8.0in lateral, 6.0in vertical (Excessive movement)
428
+ - Speed Generation: Mixed
429
+
430
+ **50-60% Level Amateur (Male #1 - Body-Dominant):**
431
+ - Hip Rotation: 90°, Shoulder Rotation: 84.8° (Great hip turn, limited shoulder)
432
+ - Posture Score: 90.7%, Weight Shift: 90.0% (Solid fundamentals)
433
+ - Arm Extension: 100.0%, Wrist Hinge: 66.8° (Good extension and lag)
434
+ - Energy Transfer: 91.8%, Power Accumulation: 100.0% (Strong power generation)
435
+ - Potential Distance: 290 yards, Sequential Kinematic: 100.0%
436
+ - Hip Thrust: 100.0%, Ground Force: 90.0% (Excellent lower body)
437
+ - Speed Generation: Body-dominant
438
+
439
+ **50-60% Level Amateur (Male #2 - Body-Dominant):**
440
+ - Hip Rotation: 90°, Shoulder Rotation: 120° (Excellent rotation both)
441
+ - Posture Score: 89.3%, Weight Shift: 90.0% (Good fundamentals)
442
+ - Arm Extension: 99.6%, Wrist Hinge: 52.6° (Great extension, limited lag)
443
+ - Energy Transfer: 96.7%, Power Accumulation: 100.0% (Excellent coordination)
444
+ - Potential Distance: 296 yards, Sequential Kinematic: 100.0%
445
+ - Tempo Issues: Very fast downswing (2.86 ratio vs ideal ~0.3)
446
+ - Speed Generation: Body-dominant
447
+
448
+ **50-60% Level Amateur (Female - Arms-Dominant):**
449
+ - Hip Rotation: 25°, Shoulder Rotation: 60° (Limited body rotation)
450
+ - Posture Score: 80.6%, Weight Shift: 50.0% (Needs improvement)
451
+ - Arm Extension: 94.8%, Wrist Hinge: 116.6° (Good extension, excellent lag)
452
+ - Energy Transfer: 56.8%, Power Accumulation: 89.3% (Mixed efficiency)
453
+ - Potential Distance: 241 yards, Sequential Kinematic: 66.8%
454
+ - Head Movement: 3.0in lateral, 2.0in vertical (Good head control)
455
+ - Ground Force: 50.0%, Hip Thrust: 30.0% (Weak lower body)
456
+ - Speed Generation: Arms-dominant
457
+
458
+ **CRITICAL INSIGHTS FROM AMATEUR ANALYSIS:**
459
+ 1. **Hip Rotation Varies Significantly**: From 23-90° in amateurs vs 60-90° in professionals
460
+ 2. **Shoulder Rotation Range**: 60-120° in amateurs, professionals consistently at 120°
461
+ 3. **Wrist Hinge Compensation**: Some amateurs (116.6°) exceed professional standards to compensate for limited body rotation
462
+ 4. **Power Generation Methods**: Body-dominant amateurs can achieve near-professional distances despite technical limitations
463
+ 5. **Head Movement Control**: Varies dramatically (3-8 inches) - major differentiator
464
+ 6. **Energy Transfer Efficiency**: Ranges from 56.8-96.7% in amateurs vs 88-96% in professionals
465
+ 7. **Weight Transfer Issues**: Some amateurs struggle with weight shift (50% vs professional 70-88%)
466
+
467
+ ## CURRENT SWING ANALYSIS
468
+
469
+ ### Swing Phase Breakdown
470
+ """.format(
471
+ swing_phases.get("setup", {}).get("frame_count", 44),
472
+ swing_phases.get("backswing", {}).get("frame_count", 7),
473
+ swing_phases.get("downswing", {}).get("frame_count", 12),
474
+ swing_phases.get("impact", {}).get("frame_count", 1),
475
+ swing_phases.get("follow_through", {}).get("frame_count", 37),
476
+ timing_metrics.get("tempo_ratio", 0.6)
477
+ )
478
+
479
+ # Add swing phase details
480
+ for phase_name, phase_data in swing_phases.items():
481
+ prompt += f"- {phase_name.title()}: {phase_data.get('frame_count', 0)} frames ({phase_data.get('duration_ms', 0):.0f}ms)\n"
482
+
483
+ prompt += f"- Total Swing: {timing_metrics.get('total_swing_frames', 0)} frames ({timing_metrics.get('total_swing_time_ms', 0):.0f}ms)\n"
484
+ prompt += f"- Tempo Ratio (down:back): {timing_metrics.get('tempo_ratio', 1.0)}\n"
485
+ prompt += f"- Estimated Club Speed: {timing_metrics.get('estimated_club_speed_mph', 85)} mph\n"
486
 
487
  # Core body mechanics
488
+ prompt += "\n### Core Body Mechanics\n"
489
+ prompt += f"- Hip Rotation: {bio_metrics.get('hip_rotation_degrees', 25)}°\n"
490
+ prompt += f"- Shoulder Rotation: {bio_metrics.get('shoulder_rotation_degrees', 60)}°\n"
491
+ prompt += f"- Posture Score: {bio_metrics.get('posture_score_percent', 60)}%\n"
492
+ prompt += f"- Weight Shift (lead foot at impact): {bio_metrics.get('weight_shift_percent', 50)}%\n"
 
 
 
 
493
 
494
  # Upper body mechanics
495
  prompt += "\n### Upper Body Mechanics\n"
496
+ prompt += f"- Arm Extension: {bio_metrics.get('arm_extension_percent', 60)}%\n"
497
+ prompt += f"- Wrist Hinge: {bio_metrics.get('wrist_hinge_degrees', 60)}°\n"
498
+ prompt += f"- Shoulder Plane Consistency: {bio_metrics.get('swing_plane_consistency_percent', 60)}%\n"
499
+ prompt += f"- Chest Rotation Efficiency: {bio_metrics.get('chest_rotation_efficiency_percent', 60)}%\n"
500
+ prompt += f"- Head Movement (lateral): {bio_metrics.get('head_movement_lateral_inches', 3.0)}in\n"
501
+ prompt += f"- Head Movement (vertical): {bio_metrics.get('head_movement_vertical_inches', 2.0)}in\n"
 
 
 
 
 
 
 
502
 
503
  # Lower body mechanics
504
  prompt += "\n### Lower Body Mechanics\n"
505
+ prompt += f"- Knee Flexion (address): {bio_metrics.get('knee_flexion_address_degrees', 25)}°\n"
506
+ prompt += f"- Knee Flexion (impact): {bio_metrics.get('knee_flexion_impact_degrees', 30)}°\n"
507
+ prompt += f"- Hip Thrust (impact): {bio_metrics.get('hip_thrust_percent', 50)}%\n"
508
+ prompt += f"- Ground Force Efficiency: {bio_metrics.get('ground_force_efficiency_percent', 60)}%\n"
509
+
510
+ # Advanced coordination metrics
511
+ prompt += "\n### Movement Quality & Timing\n"
512
+ prompt += f"- Transition Smoothness: {bio_metrics.get('transition_smoothness_percent', 60)}%\n"
513
+ prompt += f"- Sequential Kinematic Sequence: {bio_metrics.get('kinematic_sequence_percent', 60)}%\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
 
515
  # Efficiency and power metrics
516
  prompt += "\n### Efficiency & Power Metrics\n"
517
+ prompt += f"- Energy Transfer Efficiency: {bio_metrics.get('energy_transfer_efficiency_percent', 60)}%\n"
518
+ prompt += f"- Power Accumulation: {bio_metrics.get('power_accumulation_percent', 60)}%\n"
519
+ prompt += f"- Potential Distance: {bio_metrics.get('potential_distance_yards', 200)} yards\n"
520
+ prompt += f"- Speed Generation Method: {bio_metrics.get('speed_generation_method', 'Mixed')}\n"
 
 
 
 
521
 
522
  prompt += """
523
 
524
+ ## ANALYSIS INSTRUCTIONS
525
+
526
+ Using the professional benchmarks and amateur examples above as your calibration reference, provide your analysis in the following EXACT structured format:
527
+
528
+ **PERFORMANCE_CLASSIFICATION:** [XX%] (where XX is a percentage from 10% to 100%)
529
+
530
+ **STRENGTHS:**
531
+ • [Specific strength with direct comparison to professional/amateur benchmarks - e.g. "Hip rotation of 45° approaches professional range (60-90°) and exceeds most amateur examples (23-90°)"]
532
+ • [Another strength with benchmark comparison - e.g. "Energy transfer efficiency of 88% meets professional standards (88-96%) and surpasses amateur range (56.8-96.7%)"]
533
+ [Third strength with specific metric comparison to benchmarks]
534
+
535
+ **WEAKNESSES:**
536
+ • [Specific weakness with gap from professional standard - e.g. "Wrist hinge of 35° falls significantly below professional range (95-120°) and amateur compensation patterns (116.6°)"]
537
+ • [Another weakness with professional/amateur comparison - e.g. "Head movement of 12 inches exceeds both professional (2-8in) and amateur examples (3-8in)"]
538
+ [Third weakness with benchmark gap analysis]
539
+
540
+ **PRIORITY_IMPROVEMENTS:**
541
+ 1. [Most Critical] Topic Name - Current metric vs professional benchmark vs amateur examples, specific target improvement to reach next level
542
+ 2. [Important] Topic Name - Current performance vs benchmarks, actionable steps to improve toward professional standards
543
+ 3. [Focus Area] Topic Name - Current state vs benchmark ranges, realistic improvement goals based on amateur progression examples
544
+
545
+ **MANDATORY REQUIREMENTS FOR EACH SECTION:**
546
+
547
+ **For STRENGTHS** - Must include:
548
+ - Specific metric values from current analysis
549
+ - Direct comparison to professional benchmarks (60-90° hip rotation, 95-120° wrist hinge, 88-96% energy transfer, etc.)
550
+ - Comparison to amateur examples where relevant
551
+ - Recognition when metrics meet or exceed professional standards
552
+ - Acknowledgment when metrics surpass typical amateur performance
553
+
554
+ **For WEAKNESSES** - Must include:
555
+ - Specific metric gaps from professional standards
556
+ - Comparison to amateur examples to show relative standing
557
+ - Quantified differences (e.g., "15° below professional minimum," "20% gap from professional range")
558
+ - Impact on overall performance potential
559
+
560
+ **For PRIORITY_IMPROVEMENTS** - Must include:
561
+ - Current metric value vs professional benchmark range
562
+ - Reference to amateur examples showing improvement potential
563
+ - Specific target values based on professional standards
564
+ - Realistic progression steps based on amateur improvement patterns
565
+ - Clear explanation of why this improvement would impact overall performance
566
+
567
+ **EXAMPLE ANALYSIS STRUCTURE:**
568
+
569
+ **STRENGTHS:**
570
+ • Shoulder rotation of 118° nearly matches professional standard (120°) and exceeds many amateur examples (60-120° range)
571
+ • Weight shift of 85% falls within professional range (70-88%) and surpasses amateur struggles (50-90% range)
572
+
573
+ **WEAKNESSES:**
574
+ • Hip rotation of 28° falls significantly below professional minimum (60°) and amateur body-dominant examples (90°)
575
+ • Energy transfer of 62% below professional range (88-96%) and some amateur achievements (96.7%)
576
+
577
+ **PRIORITY_IMPROVEMENTS:**
578
+ 1. [Most Critical] Hip Mobility Development - Current 28° vs professional 60-90° and amateur body-dominant 90°. Target 45° as next milestone toward professional range.
579
+ 2. [Important] Kinematic Sequence Optimization - Current 70% vs professional 100% and amateur range 66.8-100%. Improve to 85% through better hip-shoulder coordination.
580
+
581
+ PERFORMANCE CLASSIFICATION SCALE:
582
+ - **90-100%**: Professional/Tour level - Consistently meets or exceeds professional benchmarks across all metrics
583
+ - **80-89%**: Advanced amateur - Meets most professional standards with minor gaps in 1-2 areas
584
+ - **70-79%**: Skilled amateur - Solid fundamentals with some gaps from professional standards
585
+ - **60-69%**: Intermediate - Good basic mechanics but several areas need improvement to reach professional level
586
+ - **50-59%**: Developing intermediate - Basic swing structure present but multiple areas below professional standards
587
+ - **40-49%**: Advanced beginner - Some fundamentals in place but significant gaps in most areas
588
+ - **30-39%**: Beginner - Basic swing motion present but major improvements needed across most metrics
589
+ - **20-29%**: Novice - Limited swing fundamentals, extensive work needed on basic mechanics
590
+ - **10-19%**: Complete beginner - Minimal swing structure, needs comprehensive fundamental development
591
+
592
+ IMPORTANT ANALYSIS PRIORITIES (Based on Real Professional Data):
593
+ 1. **PRIMARY FOCUS - Critical Biomechanical Differentiators**:
594
+ - Hip Rotation (Professional: 60-90°, Amateur Range: 23-90°) - MOST IMPORTANT
595
+ - Shoulder Rotation (Professional: 120°, Amateur Range: 60-120°) - MOST IMPORTANT
596
+ - Sequential Kinematic Sequence (Professional: 100%, Amateur Range: 66.8-100%)
597
+ - Energy Transfer Efficiency (Professional: 88-96%, Amateur Range: 56.8-96.7%)
598
+
599
+ 2. **SECONDARY FOCUS - Power Generation Mechanics**:
600
+ - Power Accumulation (Professional: 100%, Amateur Range: 82.1-100%)
601
+ - Chest Rotation Efficiency (Professional: 100%, Amateur Range: 53.7-100%)
602
+ - Wrist Hinge (Professional: 95-120°, Amateur Range: 49.4-116.6°)
603
+ - Swing Plane Consistency (Professional: 85%, Amateur: 70-85%)
604
+
605
+ 3. **TERTIARY FOCUS - Refinement Metrics**:
606
+ - Posture Score (Professional: 95-98%, Amateur Range: 80.6-90.7%)
607
+ - Arm Extension (Professional: 96-100%, Amateur Range: 94.8-100%)
608
+ - Weight Shift (Professional: 70-88%, Amateur Range: 50-90%)
609
+ - Ground Force Efficiency (Professional: 70-88%, Amateur Range: 50-90%)
610
+
611
+ 4. **DE-EMPHASIZE - Timing Variables**: Frame counts, tempo ratios, and duration metrics vary significantly based on video capture rates and personal style preferences
612
+
613
+ **SCORING CALIBRATION GUIDELINES:**
614
+ - **Hip/Shoulder Rotation Analysis**: Compare to professional minimums (60° hip, 120° shoulder) and amateur ranges
615
+ - **Energy Transfer <70%**: Score below 60%, compare to amateur range (56.8-96.7%)
616
+ - **Sequential Kinematic <80%**: Score below 70%, reference amateur examples (66.8-100%)
617
+ - **Power Accumulation <90%**: Score below 80%, compare to amateur achievements (82.1-100%)
618
+ - **Head Movement >10 inches**: Major limitation, compare to professional (2-8in) and amateur (3-8in) ranges
619
+ - **Weight Shift <60%**: Significant weakness, reference amateur struggles (50%) vs successes (90%)
620
+
621
+ IMPORTANT FORMATTING RULES:
622
+ - Use the exact headers shown above (PERFORMANCE_CLASSIFICATION, STRENGTHS, WEAKNESSES, PRIORITY_IMPROVEMENTS)
623
+ - For performance classification, use format: [XX%] where XX is the percentage (10-100)
624
+ - For strengths and weaknesses, use bullet points (•)
625
+ - For priority improvements, use numbered format (1., 2., 3.) with priority level in brackets
626
+ - Each priority improvement must have: [Priority Level] Topic Name - Full description with benchmark comparisons
627
+ - **MANDATORY**: Include specific metric values and benchmark comparisons in every strength, weakness, and improvement
628
+ - **MANDATORY**: Reference professional standards and amateur examples in analysis content
629
+ - Provide complete sentences with quantified comparisons - no generic statements
630
+ - Focus analysis on biomechanical consistency rather than timing variations
631
+ - **CRITICAL**: Every analysis point must tie back to the professional benchmarks and amateur examples provided
632
+
633
+ Remember: Use the professional benchmarks (Atthaya Thitikul: 63.4° hip, 120° shoulder, 96.1% energy transfer, etc.) and amateur examples (23-90° hip rotation range, 56.8-96.7% energy transfer range, etc.) as the foundation for ALL analysis content, not just the percentage classification. Every strength, weakness, and improvement recommendation must include specific metric comparisons to these established benchmarks.
634
  """
635
 
636
  return prompt
637
+
638
+
639
+ def parse_and_format_analysis(raw_analysis):
640
+ """
641
+ Parse the raw LLM analysis and format it into structured components
642
+
643
+ Args:
644
+ raw_analysis (str): Raw analysis text from LLM
645
+
646
+ Returns:
647
+ dict: Structured analysis with classification, strengths/weaknesses, and priorities
648
+ """
649
+ # Default structure
650
+ formatted_analysis = {
651
+ 'classification': 50, # Default to 50%
652
+ 'strengths': [],
653
+ 'weaknesses': [],
654
+ 'priority_improvements': []
655
+ }
656
+
657
+ # Extract percentage classification using the new structured format
658
+ classification_match = re.search(r'\*\*PERFORMANCE_CLASSIFICATION:\*\*\s*\[?(\d+)%?\]?', raw_analysis, re.IGNORECASE)
659
+ if classification_match:
660
+ percentage = int(classification_match.group(1))
661
+ # Ensure percentage is within valid range
662
+ formatted_analysis['classification'] = max(10, min(100, percentage))
663
+ else:
664
+ # Fallback to look for standalone percentages
665
+ percentage_patterns = [
666
+ r'(?:Performance|Classification|Level|Score).*?(\d+)%',
667
+ r'(\d+)%.*?(?:level|performance|classification)',
668
+ r'classified.*?(\d+)%',
669
+ r'(?:at|as)\s+(\d+)%'
670
+ ]
671
+
672
+ for pattern in percentage_patterns:
673
+ match = re.search(pattern, raw_analysis, re.IGNORECASE)
674
+ if match:
675
+ percentage = int(match.group(1))
676
+ formatted_analysis['classification'] = max(10, min(100, percentage))
677
+ break
678
+
679
+ # Extract strengths using the new structured format
680
+ strengths_match = re.search(r'\*\*STRENGTHS:\*\*\s*(.*?)(?=\*\*WEAKNESSES:\*\*|\*\*PRIORITY_IMPROVEMENTS:\*\*|$)', raw_analysis, re.IGNORECASE | re.DOTALL)
681
+ if strengths_match:
682
+ strengths_text = strengths_match.group(1)
683
+ # Extract bullet points
684
+ strength_items = re.findall(r'•\s*([^\n•]+)', strengths_text)
685
+ formatted_analysis['strengths'] = [item.strip() for item in strength_items if item.strip()]
686
+
687
+ # Extract weaknesses using the new structured format
688
+ weaknesses_match = re.search(r'\*\*WEAKNESSES:\*\*\s*(.*?)(?=\*\*PRIORITY_IMPROVEMENTS:\*\*|$)', raw_analysis, re.IGNORECASE | re.DOTALL)
689
+ if weaknesses_match:
690
+ weaknesses_text = weaknesses_match.group(1)
691
+ # Extract bullet points
692
+ weakness_items = re.findall(r'•\s*([^\n•]+)', weaknesses_text)
693
+ formatted_analysis['weaknesses'] = [item.strip() for item in weakness_items if item.strip()]
694
+
695
+ # Extract priority improvements using the new structured format
696
+ priority_match = re.search(r'\*\*PRIORITY_IMPROVEMENTS:\*\*\s*(.*?)$', raw_analysis, re.IGNORECASE | re.DOTALL)
697
+ if priority_match:
698
+ priority_text = priority_match.group(1)
699
+ # Extract numbered items with priority levels and descriptions
700
+ priority_items = re.findall(r'(\d+)\.\s*\[(.*?)\]\s*(.*?)(?=\d+\.\s*\[|\Z)', priority_text, re.DOTALL)
701
+ for num, priority_level, description in priority_items[:3]: # Limit to 3
702
+ # Clean up the description
703
+ description = description.strip()
704
+ # Remove any trailing incomplete sentences
705
+ if description.endswith('...') or len(description.split('.')[-1].strip()) < 5:
706
+ sentences = description.split('.')
707
+ if len(sentences) > 1:
708
+ description = '.'.join(sentences[:-1]) + '.'
709
+
710
+ formatted_analysis['priority_improvements'].append({
711
+ 'rank': int(num),
712
+ 'priority_level': priority_level.strip(),
713
+ 'description': f"[{priority_level.strip()}] {description}"
714
+ })
715
+
716
+ # Fallback parsing if structured format wasn't used
717
+ if not formatted_analysis['strengths']:
718
+ # Try original parsing methods for strengths
719
+ strengths_patterns = [
720
+ r'(?:Strengths|Strong Points|Positives|Meets.*Standards)[\s\S]*?(?=(?:Weak|Priority|Improvement|Areas|$))',
721
+ r'(?:Professional Level|Exceeds.*Standards)[\s\S]*?(?=(?:Below|Weak|Priority|$))'
722
+ ]
723
+
724
+ for pattern in strengths_patterns:
725
+ match = re.search(pattern, raw_analysis, re.IGNORECASE)
726
+ if match:
727
+ strengths_section = match.group(0)
728
+ strength_items = re.findall(r'[-•]\s*([^-•\n]+)', strengths_section)
729
+ formatted_analysis['strengths'] = [item.strip() for item in strength_items[:4]]
730
+ break
731
+
732
+ if not formatted_analysis['weaknesses']:
733
+ # Try original parsing methods for weaknesses
734
+ weaknesses_patterns = [
735
+ r'(?:Weaknesses|Weak|Areas.*Improvement|Priority.*Areas|Below.*Standards)[\s\S]*?(?=(?:Recommendation|Priority|$))',
736
+ r'(?:Critical|Important|Significant.*gaps?)[\s\S]*?(?=(?:Recommendation|$))'
737
+ ]
738
+
739
+ for pattern in weaknesses_patterns:
740
+ match = re.search(pattern, raw_analysis, re.IGNORECASE)
741
+ if match:
742
+ weaknesses_section = match.group(0)
743
+ weakness_items = re.findall(r'[-•]\s*([^-•\n]+)', weaknesses_section)
744
+ formatted_analysis['weaknesses'] = [item.strip() for item in weakness_items[:4]]
745
+ break
746
+
747
+ if not formatted_analysis['priority_improvements']:
748
+ # Try original parsing methods for priorities
749
+ priority_patterns = [
750
+ r'(?:Priority.*Improvement|Critical.*Areas?)[\s\S]*?(?=(?:Recommendation|$))',
751
+ r'(?:1\..*?2\..*?3\.)', # Numbered list
752
+ r'(?:Critical|Important|Fine-tuning)[\s\S]*?(?=(?:Critical|Important|Fine-tuning|$))'
753
+ ]
754
+
755
+ for pattern in priority_patterns:
756
+ match = re.search(pattern, raw_analysis, re.IGNORECASE | re.DOTALL)
757
+ if match:
758
+ priority_text = match.group(0)
759
+ # Extract numbered items with better parsing
760
+ numbered_items = re.findall(r'(\d+)\.\s*([^1-9]+?)(?=\d+\.|$)', priority_text, re.DOTALL)
761
+ for num, item in numbered_items[:3]: # Limit to 3
762
+ # Clean up the item text
763
+ item = item.strip()
764
+ # Remove any trailing incomplete sentences
765
+ sentences = item.split('.')
766
+ if len(sentences) > 1 and len(sentences[-1].strip()) < 10:
767
+ item = '.'.join(sentences[:-1]) + '.'
768
+
769
+ formatted_analysis['priority_improvements'].append({
770
+ 'rank': int(num),
771
+ 'description': item
772
+ })
773
+ break
774
+
775
+ # If still no content found, use defaults based on classification
776
+ if not formatted_analysis['strengths']:
777
+ formatted_analysis['strengths'] = ['Swing analysis completed successfully']
778
+
779
+ if not formatted_analysis['weaknesses']:
780
+ formatted_analysis['weaknesses'] = ['Areas for improvement identified']
781
+
782
+ if not formatted_analysis['priority_improvements']:
783
+ percentage = formatted_analysis['classification']
784
+ if percentage >= 80:
785
+ formatted_analysis['priority_improvements'] = [
786
+ {'rank': 1, 'description': '[Most Critical] Technical Refinement - Fine-tune specific mechanics to achieve consistency at the highest level.'},
787
+ {'rank': 2, 'description': '[Important] Performance Optimization - Focus on maximizing efficiency and power transfer.'},
788
+ {'rank': 3, 'description': '[Focus Area] Competitive Preparation - Enhance mental game and course management skills.'}
789
+ ]
790
+ elif percentage >= 60:
791
+ formatted_analysis['priority_improvements'] = [
792
+ {'rank': 1, 'description': '[Most Critical] Kinematic Sequence Enhancement - Improve body rotation coordination to generate more power and consistency.'},
793
+ {'rank': 2, 'description': '[Important] Clubface Control - Enhance swing path consistency for better ball striking accuracy.'},
794
+ {'rank': 3, 'description': '[Focus Area] Energy Transfer Efficiency - Optimize power transfer throughout the swing to maximize distance.'}
795
+ ]
796
+ elif percentage >= 40:
797
+ formatted_analysis['priority_improvements'] = [
798
+ {'rank': 1, 'description': '[Most Critical] Fundamental Mechanics - Establish consistent posture, grip, and setup positions.'},
799
+ {'rank': 2, 'description': '[Important] Body Rotation Development - Improve hip and shoulder turn coordination.'},
800
+ {'rank': 3, 'description': '[Focus Area] Weight Transfer - Develop proper weight shift from back foot to front foot during swing.'}
801
+ ]
802
+ else: # Below 40%
803
+ formatted_analysis['priority_improvements'] = [
804
+ {'rank': 1, 'description': '[Most Critical] Basic Setup and Posture - Focus on establishing proper spine angle and athletic stance.'},
805
+ {'rank': 2, 'description': '[Important] Fundamental Swing Motion - Develop basic backswing and downswing mechanics.'},
806
+ {'rank': 3, 'description': '[Focus Area] Balance and Stability - Improve overall balance throughout the swing motion.'}
807
+ ]
808
+
809
+ return formatted_analysis
810
+
811
+
812
+ def display_formatted_analysis(analysis_data):
813
+ """
814
+ Display the formatted analysis with performance classification, strengths/weaknesses table, and priorities
815
+
816
+ Args:
817
+ analysis_data (dict): Structured analysis data from parse_and_format_analysis
818
+ """
819
+ # 1. Performance Classification with percentage-based progress bar
820
+ user_percentage = analysis_data['classification']
821
+
822
+ # Display classification in black bolded header
823
+ st.markdown(f"""
824
+ <h2 style='color: black; font-weight: bold; text-align: center; margin-bottom: 20px;'>
825
+ 🎯 Performance Score: {user_percentage}%
826
+ </h2>
827
+ """, unsafe_allow_html=True)
828
+
829
+ # Create a visual progress bar
830
+ progress_color = "#ff4444" # Red for low scores
831
+ if user_percentage >= 80:
832
+ progress_color = "#44aa44" # Green for high scores
833
+ elif user_percentage >= 60:
834
+ progress_color = "#ffdd00" # Yellow for good scores
835
+ elif user_percentage >= 40:
836
+ progress_color = "#ff8800" # Orange for medium scores
837
+
838
+ # Progress bar with percentage labels
839
+ st.markdown(f"""
840
+ <div style='margin: 20px 0;'>
841
+ <div style='display: flex; justify-content: space-between; font-size: 12px; color: #666; margin-bottom: 5px;'>
842
+ <span>10% - Complete Beginner</span>
843
+ <span>50% - Intermediate</span>
844
+ <span>100% - Professional</span>
845
+ </div>
846
+ <div style='width: 100%; background-color: #f0f0f0; border-radius: 25px; height: 30px; position: relative;'>
847
+ <div style='width: {user_percentage}%; background-color: {progress_color}; height: 30px; border-radius: 25px;
848
+ display: flex; align-items: center; justify-content: center; color: white; font-weight: bold;'>
849
+ {user_percentage}%
850
+ </div>
851
+ </div>
852
+ <div style='display: flex; justify-content: space-between; font-size: 10px; color: #888; margin-top: 5px;'>
853
+ <span>10%</span>
854
+ <span>20%</span>
855
+ <span>30%</span>
856
+ <span>40%</span>
857
+ <span>50%</span>
858
+ <span>60%</span>
859
+ <span>70%</span>
860
+ <span>80%</span>
861
+ <span>90%</span>
862
+ <span>100%</span>
863
+ </div>
864
+ </div>
865
+ """, unsafe_allow_html=True)
866
+
867
+ # Performance level description based on percentage
868
+ if user_percentage >= 90:
869
+ level_desc = "🏆 **Professional/Tour Level** - Consistently meets or exceeds professional benchmarks"
870
+ level_color = "#44aa44"
871
+ elif user_percentage >= 80:
872
+ level_desc = "🥇 **Advanced Amateur** - Meets most professional standards with minor gaps"
873
+ level_color = "#66bb44"
874
+ elif user_percentage >= 70:
875
+ level_desc = "🥈 **Skilled Amateur** - Solid fundamentals with some gaps from professional standards"
876
+ level_color = "#88cc44"
877
+ elif user_percentage >= 60:
878
+ level_desc = "🥉 **Intermediate** - Good basic mechanics but several areas need improvement"
879
+ level_color = "#ffdd00"
880
+ elif user_percentage >= 50:
881
+ level_desc = "📈 **Developing Intermediate** - Basic swing structure present"
882
+ level_color = "#ffcc00"
883
+ elif user_percentage >= 40:
884
+ level_desc = "📚 **Advanced Beginner** - Some fundamentals in place"
885
+ level_color = "#ff8800"
886
+ elif user_percentage >= 30:
887
+ level_desc = "🎯 **Beginner** - Basic swing motion present but major improvements needed"
888
+ level_color = "#ff6600"
889
+ elif user_percentage >= 20:
890
+ level_desc = "🌱 **Novice** - Limited swing fundamentals, extensive work needed"
891
+ level_color = "#ff4444"
892
+ else:
893
+ level_desc = "🚀 **Complete Beginner** - Minimal swing structure, needs comprehensive fundamental development"
894
+ level_color = "#ff2222"
895
+
896
+ st.markdown(f"""
897
+ <div style='text-align: center; padding: 15px; background-color: {level_color}20;
898
+ border-radius: 10px; margin: 20px 0; border: 2px solid {level_color};'>
899
+ <div style='color: {level_color}; font-size: 16px; font-weight: bold;'>{level_desc}</div>
900
+ </div>
901
+ """, unsafe_allow_html=True)
902
+
903
+ st.markdown("---")
904
+
905
+ # 2. Strengths and Weaknesses Table
906
+ st.subheader("⚖️ Strengths & Areas for Improvement")
907
+
908
+ # Create two columns for the table with a visual divider
909
+ col_left, col_divider, col_right = st.columns([5, 1, 5])
910
+
911
+ with col_left:
912
+ st.markdown("""
913
+ <div style='background-color: #e8f5e8; padding: 15px; border-radius: 10px; height: 100%;'>
914
+ <h4 style='color: #2d5a2d; margin-top: 0;'>✅ Strengths</h4>
915
+ """, unsafe_allow_html=True)
916
+ for strength in analysis_data['strengths']:
917
+ st.markdown(f"• {strength}")
918
+ st.markdown("</div>", unsafe_allow_html=True)
919
+
920
+ with col_divider:
921
+ st.markdown("""
922
+ <div style='width: 2px; background-color: #ddd; height: 200px; margin: 20px auto;'></div>
923
+ """, unsafe_allow_html=True)
924
+
925
+ with col_right:
926
+ st.markdown("""
927
+ <div style='background-color: #fff5e6; padding: 15px; border-radius: 10px; height: 100%;'>
928
+ <h4 style='color: #cc6600; margin-top: 0;'>⚠️ Areas for Improvement</h4>
929
+ """, unsafe_allow_html=True)
930
+ for weakness in analysis_data['weaknesses']:
931
+ st.markdown(f"• {weakness}")
932
+ st.markdown("</div>", unsafe_allow_html=True)
933
+
934
+ st.markdown("---")
935
+
936
+ # 3. Priority Improvement Areas
937
+ st.subheader("🎯 Priority Improvement Areas")
938
+
939
+ for priority in sorted(analysis_data['priority_improvements'], key=lambda x: x['rank']):
940
+ rank = priority['rank']
941
+ description = priority['description']
942
+
943
+ # Better extraction of improvement area and description
944
+ area = ""
945
+ desc = description
946
+
947
+ # Try different patterns to extract the main topic
948
+ if '[Most Critical]' in description or '[Important]' in description or '[Focus Area]' in description:
949
+ # Pattern: [Priority Level] Topic - Description
950
+ pattern = r'\[(.*?)\]\s*(.*?)(?:\s*-\s*(.*))?$'
951
+ match = re.search(pattern, description)
952
+ if match:
953
+ priority_level = match.group(1)
954
+ area = match.group(2).strip()
955
+ desc = match.group(3).strip() if match.group(3) else ""
956
+ elif ':' in description:
957
+ # Pattern: Topic: Description
958
+ parts = description.split(':', 1)
959
+ area = parts[0].strip()
960
+ desc = parts[1].strip()
961
+ elif ' - ' in description:
962
+ # Pattern: Topic - Description
963
+ parts = description.split(' - ', 1)
964
+ area = parts[0].strip()
965
+ desc = parts[1].strip()
966
+ else:
967
+ # Try to extract first meaningful phrase as area
968
+ words = description.split()
969
+ if len(words) > 5:
970
+ # Take first 3-5 words as the area
971
+ area = ' '.join(words[:4])
972
+ desc = ' '.join(words[4:])
973
+ else:
974
+ area = description
975
+ desc = ""
976
+
977
+ # Clean up area and description
978
+ area = area.replace('[Most Critical]', '').replace('[Important]', '').replace('[Focus Area]', '').strip()
979
+
980
+ # Ensure we have meaningful content
981
+ if not area or len(area) < 5:
982
+ area = f"Priority {rank} Improvement"
983
+
984
+ if not desc or len(desc) < 10:
985
+ # Provide a more complete description based on the area
986
+ if 'posture' in area.lower():
987
+ desc = "Work on maintaining proper spine angle and athletic stance throughout the swing for better consistency and power transfer."
988
+ elif 'tempo' in area.lower() or 'timing' in area.lower():
989
+ desc = "Focus on developing a smooth, consistent rhythm that allows for proper sequencing of body movements."
990
+ elif 'rotation' in area.lower():
991
+ desc = "Improve the coordination and range of motion in your body turn to generate more power and accuracy."
992
+ elif 'weight' in area.lower() or 'shift' in area.lower():
993
+ desc = "Practice transferring weight from back foot to front foot during the swing for better balance and power."
994
+ elif 'knee' in area.lower():
995
+ desc = "Work on maintaining proper knee flex and stability throughout the swing for better foundation and consistency."
996
+ elif 'hip' in area.lower():
997
+ desc = "Focus on improving hip mobility and thrust timing to enhance power generation and sequencing."
998
+ elif 'chest' in area.lower():
999
+ desc = "Improve chest rotation efficiency to better coordinate upper body movement with the swing sequence."
1000
+ else:
1001
+ desc = description # Use the full description if we can't categorize it
1002
+
1003
+ # Display using Streamlit's native components
1004
+ if rank == 1:
1005
+ st.error(f"**{rank}. MOST CRITICAL: {area}**")
1006
+ st.write(desc)
1007
+ elif rank == 2:
1008
+ st.warning(f"**{rank}. IMPORTANT: {area}**")
1009
+ st.write(desc)
1010
+ else:
1011
+ st.info(f"**{rank}. FOCUS AREA: {area}**")
1012
+ st.write(desc)
1013
+
1014
+ st.write("") # Add spacing between items
1015
+
1016
+
1017
+ def calculate_biomechanical_metrics(pose_data, swing_phases):
1018
+ """
1019
+ Calculate biomechanical metrics from pose keypoints data
1020
+
1021
+ Args:
1022
+ pose_data (dict): Dictionary mapping frame indices to pose keypoints
1023
+ swing_phases (dict): Dictionary mapping phase names to lists of frame indices
1024
+
1025
+ Returns:
1026
+ dict: Calculated biomechanical metrics
1027
+ """
1028
+ metrics = {}
1029
+
1030
+ # Get key frames for analysis
1031
+ setup_frames = swing_phases.get("setup", [])
1032
+ backswing_frames = swing_phases.get("backswing", [])
1033
+ downswing_frames = swing_phases.get("downswing", [])
1034
+ impact_frames = swing_phases.get("impact", [])
1035
+
1036
+ # Get representative frames
1037
+ setup_frame = setup_frames[len(setup_frames) // 2] if setup_frames else None
1038
+ top_backswing_frame = backswing_frames[-1] if backswing_frames else None
1039
+ impact_frame = impact_frames[0] if impact_frames else None
1040
+
1041
+ # MediaPipe Pose landmark indices
1042
+ # Shoulders: left(11), right(12)
1043
+ # Hips: left(23), right(24)
1044
+ # Knees: left(25), right(26)
1045
+ # Ankles: left(27), right(28)
1046
+ # Elbows: left(13), right(14)
1047
+ # Wrists: left(15), right(16)
1048
+
1049
+ try:
1050
+ # Calculate Hip Rotation
1051
+ if setup_frame and top_backswing_frame and setup_frame in pose_data and top_backswing_frame in pose_data:
1052
+ setup_keypoints = pose_data[setup_frame]
1053
+ backswing_keypoints = pose_data[top_backswing_frame]
1054
+
1055
+ if len(setup_keypoints) >= 33 and len(backswing_keypoints) >= 33:
1056
+ # Hip rotation calculation using hip landmarks
1057
+ setup_left_hip = np.array(setup_keypoints[23][:2])
1058
+ setup_right_hip = np.array(setup_keypoints[24][:2])
1059
+ backswing_left_hip = np.array(backswing_keypoints[23][:2])
1060
+ backswing_right_hip = np.array(backswing_keypoints[24][:2])
1061
+
1062
+ # Calculate hip line angles
1063
+ setup_hip_vector = setup_right_hip - setup_left_hip
1064
+ backswing_hip_vector = backswing_right_hip - backswing_left_hip
1065
+
1066
+ setup_hip_angle = np.degrees(np.arctan2(setup_hip_vector[1], setup_hip_vector[0]))
1067
+ backswing_hip_angle = np.degrees(np.arctan2(backswing_hip_vector[1], backswing_hip_vector[0]))
1068
+
1069
+ hip_rotation = abs(backswing_hip_angle - setup_hip_angle)
1070
+ # Normalize to reasonable range (professionals typically achieve 45+ degrees)
1071
+ metrics["hip_rotation"] = min(hip_rotation, 90)
1072
+ else:
1073
+ metrics["hip_rotation"] = 25 # Lower default for incomplete data
1074
+ else:
1075
+ metrics["hip_rotation"] = 25
1076
+
1077
+ # Calculate Shoulder Rotation
1078
+ if setup_frame and top_backswing_frame and setup_frame in pose_data and top_backswing_frame in pose_data:
1079
+ setup_keypoints = pose_data[setup_frame]
1080
+ backswing_keypoints = pose_data[top_backswing_frame]
1081
+
1082
+ if len(setup_keypoints) >= 33 and len(backswing_keypoints) >= 33:
1083
+ # Shoulder rotation calculation
1084
+ setup_left_shoulder = np.array(setup_keypoints[11][:2])
1085
+ setup_right_shoulder = np.array(setup_keypoints[12][:2])
1086
+ backswing_left_shoulder = np.array(backswing_keypoints[11][:2])
1087
+ backswing_right_shoulder = np.array(backswing_keypoints[12][:2])
1088
+
1089
+ setup_shoulder_vector = setup_right_shoulder - setup_left_shoulder
1090
+ backswing_shoulder_vector = backswing_right_shoulder - backswing_left_shoulder
1091
+
1092
+ setup_shoulder_angle = np.degrees(np.arctan2(setup_shoulder_vector[1], setup_shoulder_vector[0]))
1093
+ backswing_shoulder_angle = np.degrees(np.arctan2(backswing_shoulder_vector[1], backswing_shoulder_vector[0]))
1094
+
1095
+ shoulder_rotation = abs(backswing_shoulder_angle - setup_shoulder_angle)
1096
+ metrics["shoulder_rotation"] = min(shoulder_rotation, 120)
1097
+ else:
1098
+ metrics["shoulder_rotation"] = 60 # Lower default
1099
+ else:
1100
+ metrics["shoulder_rotation"] = 60
1101
+
1102
+ # Calculate Weight Shift (using hip and ankle positions)
1103
+ if setup_frame and impact_frame and setup_frame in pose_data and impact_frame in pose_data:
1104
+ setup_keypoints = pose_data[setup_frame]
1105
+ impact_keypoints = pose_data[impact_frame]
1106
+
1107
+ if len(setup_keypoints) >= 33 and len(impact_keypoints) >= 33:
1108
+ # Use center of mass approximation
1109
+ setup_left_ankle = np.array(setup_keypoints[27][:2])
1110
+ setup_right_ankle = np.array(setup_keypoints[28][:2])
1111
+ impact_left_ankle = np.array(impact_keypoints[27][:2])
1112
+ impact_right_ankle = np.array(impact_keypoints[28][:2])
1113
+
1114
+ # Calculate weight distribution based on foot positioning
1115
+ setup_center = (setup_left_ankle + setup_right_ankle) / 2
1116
+ impact_center = (impact_left_ankle + impact_right_ankle) / 2
1117
+
1118
+ # Weight shift calculation (simplified)
1119
+ foot_width = np.linalg.norm(setup_right_ankle - setup_left_ankle)
1120
+ if foot_width > 0:
1121
+ weight_shift_amount = np.linalg.norm(impact_center - setup_center) / foot_width
1122
+ # Convert to percentage (professionals typically achieve 70%+ to front foot)
1123
+ weight_shift = min(0.5 + weight_shift_amount * 0.5, 0.9)
1124
+ else:
1125
+ weight_shift = 0.5
1126
+ metrics["weight_shift"] = weight_shift
1127
+ else:
1128
+ metrics["weight_shift"] = 0.5 # Neutral default
1129
+ else:
1130
+ metrics["weight_shift"] = 0.5
1131
+
1132
+ # Calculate Posture Score (spine angle consistency)
1133
+ posture_scores = []
1134
+ for frame_list in [setup_frames, backswing_frames, impact_frames]:
1135
+ if frame_list:
1136
+ frame = frame_list[len(frame_list) // 2]
1137
+ if frame in pose_data and len(pose_data[frame]) >= 33:
1138
+ keypoints = pose_data[frame]
1139
+ # Use shoulder and hip landmarks to estimate spine angle
1140
+ left_shoulder = np.array(keypoints[11][:2])
1141
+ right_shoulder = np.array(keypoints[12][:2])
1142
+ left_hip = np.array(keypoints[23][:2])
1143
+ right_hip = np.array(keypoints[24][:2])
1144
+
1145
+ shoulder_center = (left_shoulder + right_shoulder) / 2
1146
+ hip_center = (left_hip + right_hip) / 2
1147
+
1148
+ spine_vector = shoulder_center - hip_center
1149
+ spine_angle = np.degrees(np.arctan2(spine_vector[1], spine_vector[0]))
1150
+ posture_scores.append(abs(spine_angle))
1151
+
1152
+ if posture_scores:
1153
+ # Good posture = consistent spine angle across phases
1154
+ posture_consistency = 1.0 - (np.std(posture_scores) / 90.0) # Normalize by 90 degrees
1155
+ metrics["posture_score"] = max(0.3, min(posture_consistency, 1.0))
1156
+ else:
1157
+ metrics["posture_score"] = 0.6
1158
+
1159
+ # Calculate Arm Extension at Impact
1160
+ if impact_frame and impact_frame in pose_data and len(pose_data[impact_frame]) >= 33:
1161
+ keypoints = pose_data[impact_frame]
1162
+ right_shoulder = np.array(keypoints[12][:2])
1163
+ right_elbow = np.array(keypoints[14][:2])
1164
+ right_wrist = np.array(keypoints[16][:2])
1165
+
1166
+ # Calculate arm extension
1167
+ upper_arm = np.linalg.norm(right_elbow - right_shoulder)
1168
+ forearm = np.linalg.norm(right_wrist - right_elbow)
1169
+ total_arm_length = upper_arm + forearm
1170
+
1171
+ # Calculate actual distance from shoulder to wrist
1172
+ actual_distance = np.linalg.norm(right_wrist - right_shoulder)
1173
+
1174
+ if total_arm_length > 0:
1175
+ extension_ratio = actual_distance / total_arm_length
1176
+ metrics["arm_extension"] = min(extension_ratio, 1.0)
1177
+ else:
1178
+ metrics["arm_extension"] = 0.6
1179
+ else:
1180
+ metrics["arm_extension"] = 0.6
1181
+
1182
+ # Calculate Wrist Hinge using joint angles
1183
+ wrist_angles = []
1184
+ for frame_list in [backswing_frames, impact_frames]:
1185
+ if frame_list:
1186
+ frame = frame_list[len(frame_list) // 2]
1187
+ if frame in pose_data:
1188
+ angles = calculate_joint_angles(pose_data[frame])
1189
+ if "right_wrist" in angles:
1190
+ wrist_angles.append(angles["right_wrist"])
1191
+
1192
+ if wrist_angles:
1193
+ avg_wrist_angle = np.mean(wrist_angles)
1194
+ # Good wrist hinge is typically 80+ degrees
1195
+ metrics["wrist_hinge"] = min(avg_wrist_angle, 120)
1196
+ else:
1197
+ metrics["wrist_hinge"] = 60
1198
+
1199
+ # Calculate Head Movement (lateral and vertical)
1200
+ if setup_frame and impact_frame and setup_frame in pose_data and impact_frame in pose_data:
1201
+ setup_keypoints = pose_data[setup_frame]
1202
+ impact_keypoints = pose_data[impact_frame]
1203
+
1204
+ if len(setup_keypoints) >= 33 and len(impact_keypoints) >= 33:
1205
+ # Use nose landmark (index 0) for head position
1206
+ setup_head = np.array(setup_keypoints[0][:2])
1207
+ impact_head = np.array(impact_keypoints[0][:2])
1208
+
1209
+ head_movement = np.abs(impact_head - setup_head)
1210
+ # Convert pixel movement to approximate inches (rough estimation)
1211
+ # Assume average person's head is about 9 inches, use that as scale
1212
+ if len(setup_keypoints) > 10: # Have enough landmarks
1213
+ head_height_pixels = abs(setup_keypoints[0][1] - setup_keypoints[10][1]) # Nose to mouth
1214
+ if head_height_pixels > 0:
1215
+ pixel_to_inch = 4.0 / head_height_pixels # Approximate nose-to-mouth is 4 inches
1216
+ lateral_movement = head_movement[0] * pixel_to_inch
1217
+ vertical_movement = head_movement[1] * pixel_to_inch
1218
+ else:
1219
+ lateral_movement = 3.0
1220
+ vertical_movement = 2.0
1221
+ else:
1222
+ lateral_movement = 3.0
1223
+ vertical_movement = 2.0
1224
+
1225
+ metrics["head_movement_lateral"] = min(lateral_movement, 8.0)
1226
+ metrics["head_movement_vertical"] = min(vertical_movement, 6.0)
1227
+ else:
1228
+ metrics["head_movement_lateral"] = 3.0
1229
+ metrics["head_movement_vertical"] = 2.0
1230
+ else:
1231
+ metrics["head_movement_lateral"] = 3.0
1232
+ metrics["head_movement_vertical"] = 2.0
1233
+
1234
+ # Calculate Knee Flexion
1235
+ knee_flexions = {}
1236
+ for phase_name, frame_list in [("address", setup_frames), ("impact", impact_frames)]:
1237
+ if frame_list:
1238
+ frame = frame_list[len(frame_list) // 2]
1239
+ if frame in pose_data and len(pose_data[frame]) >= 33:
1240
+ keypoints = pose_data[frame]
1241
+ # Right knee angle using hip, knee, ankle
1242
+ right_hip = np.array(keypoints[24][:2])
1243
+ right_knee = np.array(keypoints[26][:2])
1244
+ right_ankle = np.array(keypoints[28][:2])
1245
+
1246
+ # Calculate knee angle
1247
+ thigh_vector = right_hip - right_knee
1248
+ shin_vector = right_ankle - right_knee
1249
+
1250
+ if np.linalg.norm(thigh_vector) > 0 and np.linalg.norm(shin_vector) > 0:
1251
+ cos_angle = np.dot(thigh_vector, shin_vector) / (np.linalg.norm(thigh_vector) * np.linalg.norm(shin_vector))
1252
+ cos_angle = np.clip(cos_angle, -1, 1)
1253
+ knee_angle = np.degrees(np.arccos(cos_angle))
1254
+ knee_flexions[phase_name] = min(knee_angle, 60)
1255
+ else:
1256
+ knee_flexions[phase_name] = 25
1257
+ else:
1258
+ knee_flexions[phase_name] = 25
1259
+
1260
+ metrics["knee_flexion_address"] = knee_flexions.get("address", 25)
1261
+ metrics["knee_flexion_impact"] = knee_flexions.get("impact", 30)
1262
+
1263
+ # Calculate derived metrics based on quality of basic metrics
1264
+ # These are more complex and would require additional analysis
1265
+
1266
+ # Swing Plane Consistency (based on arm and club positions across frames)
1267
+ if metrics["shoulder_rotation"] >= 80 and metrics["arm_extension"] >= 0.75:
1268
+ metrics["swing_plane_consistency"] = 0.85
1269
+ elif metrics["shoulder_rotation"] >= 60 and metrics["arm_extension"] >= 0.6:
1270
+ metrics["swing_plane_consistency"] = 0.70
1271
+ else:
1272
+ metrics["swing_plane_consistency"] = 0.55
1273
+
1274
+ # Chest Rotation Efficiency (derived from shoulder rotation and posture)
1275
+ chest_efficiency = (metrics["shoulder_rotation"] / 90.0) * metrics["posture_score"]
1276
+ metrics["chest_rotation_efficiency"] = min(chest_efficiency, 1.0)
1277
+
1278
+ # Hip Thrust (derived from weight shift and hip rotation)
1279
+ hip_thrust = (metrics["weight_shift"] - 0.5) * 2 * (metrics["hip_rotation"] / 45.0)
1280
+ metrics["hip_thrust"] = max(0.3, min(hip_thrust, 1.0))
1281
+
1282
+ # Ground Force Efficiency (derived from weight shift and knee flexion consistency)
1283
+ knee_consistency = 1.0 - abs(metrics["knee_flexion_impact"] - metrics["knee_flexion_address"]) / 30.0
1284
+ ground_force = metrics["weight_shift"] * knee_consistency
1285
+ metrics["ground_force_efficiency"] = max(0.4, min(ground_force, 1.0))
1286
+
1287
+ # Transition Smoothness (based on posture consistency and movement quality)
1288
+ head_movement_penalty = (metrics["head_movement_lateral"] + metrics["head_movement_vertical"]) / 10.0
1289
+ transition_smoothness = metrics["posture_score"] * (1.0 - head_movement_penalty)
1290
+ metrics["transition_smoothness"] = max(0.4, min(transition_smoothness, 1.0))
1291
+
1292
+ # Sequential Kinematic Sequence (based on overall coordination)
1293
+ coordination_score = (metrics["hip_rotation"] / 45.0 + metrics["shoulder_rotation"] / 90.0 +
1294
+ metrics["weight_shift"] + metrics["arm_extension"]) / 4.0
1295
+ metrics["kinematic_sequence"] = max(0.5, min(coordination_score, 1.0))
1296
+
1297
+ # Energy Transfer Efficiency (based on multiple factors)
1298
+ energy_transfer = (metrics["kinematic_sequence"] + metrics["ground_force_efficiency"] +
1299
+ metrics["chest_rotation_efficiency"]) / 3.0
1300
+ metrics["energy_transfer"] = max(0.4, min(energy_transfer, 1.0))
1301
+
1302
+ # Power Accumulation (based on body mechanics)
1303
+ power_accumulation = (metrics["hip_rotation"] / 45.0 + metrics["shoulder_rotation"] / 90.0 +
1304
+ metrics["wrist_hinge"] / 80.0) / 3.0
1305
+ metrics["power_accumulation"] = max(0.4, min(power_accumulation, 1.0))
1306
+
1307
+ # Potential Distance (based on power metrics and efficiency)
1308
+ base_distance = 180 # Base amateur distance
1309
+ power_multiplier = metrics["power_accumulation"] * metrics["energy_transfer"]
1310
+ potential_distance = base_distance + (power_multiplier * 120) # Up to 300 yards for perfect mechanics
1311
+ metrics["potential_distance"] = min(potential_distance, 320)
1312
+
1313
+ # Speed Generation Method (based on power sources)
1314
+ if metrics["hip_rotation"] >= 40 and metrics["shoulder_rotation"] >= 80:
1315
+ metrics["speed_generation"] = "Body-dominant"
1316
+ elif metrics["arm_extension"] >= 0.8 and metrics["wrist_hinge"] >= 75:
1317
+ metrics["speed_generation"] = "Arms-dominant"
1318
+ else:
1319
+ metrics["speed_generation"] = "Mixed"
1320
+
1321
+ except Exception as e:
1322
+ print(f"Error calculating biomechanical metrics: {str(e)}")
1323
+ # Fail here
1324
+ return None
1325
+
1326
+ return metrics
app/models/swing_analyzer.py CHANGED
@@ -23,7 +23,7 @@ def find_top_of_backswing(pose_data):
23
  return top_frame
24
 
25
 
26
- def detect_impact_frame(pose_data, detections, sample_rate=2):
27
  """
28
  Simple impact detection: ball movement first, wrist speed fallback
29
  """
@@ -42,12 +42,29 @@ def detect_impact_frame(pose_data, detections, sample_rate=2):
42
  ball_detections = [d for d in detections if d.class_name == "sports ball"]
43
  ball_positions = {}
44
 
 
 
 
 
 
 
45
  for detection in ball_detections:
46
- frame_idx = detection.frame_idx // sample_rate
47
- if frame_idx > top_backswing:
 
 
 
 
 
 
 
 
 
 
 
48
  x1, y1, x2, y2 = detection.bbox
49
  center_x, center_y = (x1 + x2) / 2, (y1 + y2) / 2
50
- ball_positions[frame_idx] = (center_x, center_y)
51
 
52
  # Find first significant ball movement
53
  if len(ball_positions) >= 2:
@@ -58,7 +75,7 @@ def detect_impact_frame(pose_data, detections, sample_rate=2):
58
  movement = np.sqrt((curr_pos[0] - prev_pos[0])**2 + (curr_pos[1] - prev_pos[1])**2)
59
 
60
  if movement > 15: # Significant movement threshold
61
- print(f"Impact detected via ball movement at frame {sorted_frames[i]}")
62
  return sorted_frames[i]
63
 
64
  # Method 2: Wrist speed fallback (simple and reliable)
@@ -80,11 +97,11 @@ def detect_impact_frame(pose_data, detections, sample_rate=2):
80
  max_wrist_speed = wrist_speed
81
  impact_frame = curr_frame
82
 
83
- print(f"Impact detected via wrist speed at frame {impact_frame}")
84
  return impact_frame or downswing_frames[len(downswing_frames) // 3]
85
 
86
 
87
- def segment_swing_pose_based(pose_data, detections=None, sample_rate=2):
88
  """
89
  Simple swing segmentation with clean impact detection
90
  """
@@ -117,7 +134,7 @@ def segment_swing_pose_based(pose_data, detections=None, sample_rate=2):
117
  downswing_frames = [f for f in frame_indices if f > top_backswing]
118
  impact_frame = downswing_frames[len(downswing_frames) // 3] if downswing_frames else top_backswing + 1
119
 
120
- print(f"Swing phases: Setup end={setup_end}, Top backswing={top_backswing}, Impact={impact_frame}")
121
 
122
  # 4. Assign phases
123
  for idx in frame_indices:
@@ -136,14 +153,14 @@ def segment_swing_pose_based(pose_data, detections=None, sample_rate=2):
136
 
137
 
138
  # Wrapper function to maintain compatibility with existing Streamlit app
139
- def segment_swing(pose_data, detections, sample_rate=2):
140
  """
141
  Main swing segmentation function (wrapper for pose-based approach)
142
  """
143
  return segment_swing_pose_based(pose_data, detections, sample_rate)
144
 
145
 
146
- def analyze_trajectory(frames, detections, swing_phases, sample_rate=2):
147
  """
148
  Analyze ball trajectory and calculate club speed
149
  """
 
23
  return top_frame
24
 
25
 
26
+ def detect_impact_frame(pose_data, detections, sample_rate=1):
27
  """
28
  Simple impact detection: ball movement first, wrist speed fallback
29
  """
 
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:
 
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)
 
97
  max_wrist_speed = wrist_speed
98
  impact_frame = curr_frame
99
 
100
+ print(f"Impact detected via wrist speed at processed frame {impact_frame} (original frame {impact_frame * sample_rate if impact_frame else 'N/A'})")
101
  return impact_frame or downswing_frames[len(downswing_frames) // 3]
102
 
103
 
104
+ def segment_swing_pose_based(pose_data, detections=None, sample_rate=1):
105
  """
106
  Simple swing segmentation with clean impact detection
107
  """
 
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:
 
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
  """
app/streamlit_app.py CHANGED
@@ -19,11 +19,11 @@ load_dotenv()
19
  # Add the app directory to the path
20
  sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
21
 
22
- from app.utils.video_downloader import download_youtube_video, download_pro_reference
23
  from app.utils.video_processor import process_video
24
  from app.models.pose_estimator import analyze_pose
25
  from app.models.swing_analyzer import segment_swing, analyze_trajectory
26
- from app.models.llm_analyzer import generate_swing_analysis, create_llm_prompt, prepare_data_for_llm, check_llm_services
27
  from app.utils.visualizer import create_annotated_video
28
  from app.utils.comparison import create_key_frame_comparison, extract_key_swing_frames
29
 
@@ -92,6 +92,7 @@ def main():
92
  """Main Streamlit application"""
93
  st.title("Par-ity Project: Golf Swing Analysis 🏌️‍♀️")
94
  st.write("Founded to address the gender gap in golf participation and access to quality coaching resources, Par-ity Project is a technology-driven initiative empowering girls in golf through innovative AI based swing analysis. This technology uses computer vision and machine learning algorithms to analyze golf swings and provide personalized feedback to improve technique and performance.")
 
95
  # Initialize session state for storing analysis results
96
  if 'video_analyzed' not in st.session_state:
97
  st.session_state.video_analyzed = False
@@ -107,10 +108,32 @@ def main():
107
  }
108
  if 'pro_reference_path' not in st.session_state:
109
  st.session_state.pro_reference_path = None
 
 
 
 
 
 
 
110
 
111
  # Sidebar for configuration
112
  st.sidebar.title("Configuration")
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  # Check available LLM services
115
  llm_services = check_llm_services()
116
  any_service_available = llm_services['ollama'][
@@ -161,14 +184,14 @@ def main():
161
  else:
162
  st.sidebar.info("Using sample analysis mode (no LLM required)")
163
 
164
- # Frame skip rate for YOLO
165
  sample_rate = st.sidebar.slider(
166
- "Frame Skip Rate (YOLO)",
167
  min_value=1,
168
  max_value=10,
169
- value=2,
170
  help=
171
- "Process every Nth frame. Higher values = faster but less accurate.")
172
 
173
  # Pro reference toggle
174
  enable_pro_comparison = st.sidebar.checkbox(
@@ -289,6 +312,10 @@ def main():
289
  'prompt': prompt
290
  }
291
 
 
 
 
 
292
  # Present the options after analysis
293
  st.subheader("What would you like to do next?")
294
  options_col1, options_col2, options_col3 = st.columns(3)
@@ -311,6 +338,9 @@ def main():
311
  except Exception as e:
312
  st.error(f"Error during analysis: {str(e)}")
313
  st.session_state.video_analyzed = False
 
 
 
314
 
315
  # Show action buttons and their results (only if analysis is complete)
316
  if st.session_state.video_analyzed:
@@ -421,29 +451,13 @@ def main():
421
  elif llm_services['openai']['available']:
422
  st.info("🤖 **Analysis generated using OpenAI**")
423
 
424
- st.markdown(analysis)
425
-
426
- # Add some example drills based on the analysis
427
- if "Error:" not in analysis: # Only show drills if analysis was successful
428
- st.subheader("Recommended Drills")
429
- drill1, drill2 = st.columns(2)
430
-
431
- with drill1:
432
- st.markdown("**Posture Drill**")
433
- st.markdown("- Stand with your back against a wall")
434
- st.markdown(
435
- "- Take your golf stance while maintaining contact"
436
- )
437
- st.markdown(
438
- "- Practice maintaining this posture during your swing"
439
- )
440
-
441
- with drill2:
442
- st.markdown("**Tempo Drill**")
443
- st.markdown("- Count '1-2-3' for your backswing")
444
- st.markdown("- Count '1' for your downswing")
445
- st.markdown("- Practice maintaining a 3:1 tempo ratio")
446
-
447
  # Handle key frame analysis (new tab/option)
448
  if keyframe_analysis_clicked:
449
  try:
 
19
  # Add the app directory to the path
20
  sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
21
 
22
+ from app.utils.video_downloader import download_youtube_video, download_pro_reference, cleanup_video_file, cleanup_downloads_directory
23
  from app.utils.video_processor import process_video
24
  from app.models.pose_estimator import analyze_pose
25
  from app.models.swing_analyzer import segment_swing, analyze_trajectory
26
+ from app.models.llm_analyzer import generate_swing_analysis, create_llm_prompt, prepare_data_for_llm, check_llm_services, parse_and_format_analysis, display_formatted_analysis
27
  from app.utils.visualizer import create_annotated_video
28
  from app.utils.comparison import create_key_frame_comparison, extract_key_swing_frames
29
 
 
92
  """Main Streamlit application"""
93
  st.title("Par-ity Project: Golf Swing Analysis 🏌️‍♀️")
94
  st.write("Founded to address the gender gap in golf participation and access to quality coaching resources, Par-ity Project is a technology-driven initiative empowering girls in golf through innovative AI based swing analysis. This technology uses computer vision and machine learning algorithms to analyze golf swings and provide personalized feedback to improve technique and performance.")
95
+
96
  # Initialize session state for storing analysis results
97
  if 'video_analyzed' not in st.session_state:
98
  st.session_state.video_analyzed = False
 
108
  }
109
  if 'pro_reference_path' not in st.session_state:
110
  st.session_state.pro_reference_path = None
111
+
112
+ # Add session cleanup - clean up old files when starting a new session
113
+ if 'session_initialized' not in st.session_state:
114
+ cleanup_result = cleanup_downloads_directory(keep_annotated=True)
115
+ if cleanup_result.get('files_removed', 0) > 0:
116
+ st.info(f"🗑️ Cleaned up {cleanup_result['files_removed']} old files ({cleanup_result['space_freed_mb']} MB freed)")
117
+ st.session_state.session_initialized = True
118
 
119
  # Sidebar for configuration
120
  st.sidebar.title("Configuration")
121
 
122
+ # Add Reset Session button
123
+ st.sidebar.markdown("---")
124
+ if st.sidebar.button("🗑️ Reset Session & Clean Files", help="Clear all session data and remove downloaded files"):
125
+ # Clean up downloads directory
126
+ cleanup_result = cleanup_downloads_directory(keep_annotated=False) # Remove all files including annotated
127
+
128
+ # Clear session state
129
+ for key in list(st.session_state.keys()):
130
+ del st.session_state[key]
131
+
132
+ st.sidebar.success(f"Session reset! Cleaned {cleanup_result.get('files_removed', 0)} files ({cleanup_result.get('space_freed_mb', 0)} MB freed)")
133
+ st.rerun()
134
+
135
+ st.sidebar.markdown("---")
136
+
137
  # Check available LLM services
138
  llm_services = check_llm_services()
139
  any_service_available = llm_services['ollama'][
 
184
  else:
185
  st.sidebar.info("Using sample analysis mode (no LLM required)")
186
 
187
+ # Frame processing rate for YOLO
188
  sample_rate = st.sidebar.slider(
189
+ "Frame Processing Rate (YOLO)",
190
  min_value=1,
191
  max_value=10,
192
+ value=1,
193
  help=
194
+ "Process every Nth frame. 1 = all frames (most accurate), higher values = faster but less accurate.")
195
 
196
  # Pro reference toggle
197
  enable_pro_comparison = st.sidebar.checkbox(
 
312
  'prompt': prompt
313
  }
314
 
315
+ # Clean up the original video file after processing (keep frames in memory)
316
+ st.info("🗑️ Cleaning up original video file to save space...")
317
+ cleanup_video_file(video_path)
318
+
319
  # Present the options after analysis
320
  st.subheader("What would you like to do next?")
321
  options_col1, options_col2, options_col3 = st.columns(3)
 
338
  except Exception as e:
339
  st.error(f"Error during analysis: {str(e)}")
340
  st.session_state.video_analyzed = False
341
+ # Clean up on error as well
342
+ if video_path and os.path.exists(video_path):
343
+ cleanup_video_file(video_path)
344
 
345
  # Show action buttons and their results (only if analysis is complete)
346
  if st.session_state.video_analyzed:
 
451
  elif llm_services['openai']['available']:
452
  st.info("🤖 **Analysis generated using OpenAI**")
453
 
454
+ # Parse and display the formatted analysis instead of raw markdown
455
+ if "Error:" not in analysis:
456
+ formatted_analysis = parse_and_format_analysis(analysis)
457
+ display_formatted_analysis(formatted_analysis)
458
+ else:
459
+ # Show error message if analysis failed
460
+ st.error(analysis)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  # Handle key frame analysis (new tab/option)
462
  if keyframe_analysis_clicked:
463
  try:
app/utils/comparison.py CHANGED
@@ -162,6 +162,7 @@ def extract_key_swing_frames(video_path, frames, swing_phases=None):
162
  impact_idx = len(frames) // 2
163
 
164
  print(f"Key frame indices (relative to processed frames) - Setup: {setup_idx}, Backswing: {backswing_idx}, Impact: {impact_idx}")
 
165
 
166
  # Get rotation angle from the original video file
167
  rotation_angle = 0
 
162
  impact_idx = len(frames) // 2
163
 
164
  print(f"Key frame indices (relative to processed frames) - Setup: {setup_idx}, Backswing: {backswing_idx}, Impact: {impact_idx}")
165
+ print(f"These correspond to original video frames (approx) - Setup: ~{setup_idx * 1}, Backswing: ~{backswing_idx * 1}, Impact: ~{impact_idx * 1} (assuming sample_rate=1)")
166
 
167
  # Get rotation angle from the original video file
168
  rotation_angle = 0
app/utils/video_downloader.py CHANGED
@@ -6,6 +6,88 @@ import os
6
  import yt_dlp
7
 
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  def download_youtube_video(url, output_dir="downloads"):
10
  """
11
  Download a YouTube video from the provided URL using yt-dlp
 
6
  import yt_dlp
7
 
8
 
9
+ def cleanup_video_file(video_path):
10
+ """
11
+ Delete a specific video file after processing
12
+
13
+ Args:
14
+ video_path (str): Path to the video file to delete
15
+
16
+ Returns:
17
+ bool: True if file was deleted successfully, False otherwise
18
+ """
19
+ try:
20
+ if os.path.exists(video_path):
21
+ os.remove(video_path)
22
+ print(f"Cleaned up video file: {video_path}")
23
+ return True
24
+ else:
25
+ print(f"Video file not found for cleanup: {video_path}")
26
+ return False
27
+ except Exception as e:
28
+ print(f"Error cleaning up video file {video_path}: {str(e)}")
29
+ return False
30
+
31
+
32
+ def cleanup_downloads_directory(output_dir="downloads", keep_annotated=True):
33
+ """
34
+ Clean up downloaded videos from the downloads directory
35
+
36
+ Args:
37
+ output_dir (str): Directory containing downloaded videos
38
+ keep_annotated (bool): Whether to keep annotated videos (default: True)
39
+
40
+ Returns:
41
+ dict: Cleanup results with files removed and space freed
42
+ """
43
+ try:
44
+ if not os.path.exists(output_dir):
45
+ return {"files_removed": 0, "space_freed_mb": 0}
46
+
47
+ files_removed = 0
48
+ space_freed = 0
49
+
50
+ for filename in os.listdir(output_dir):
51
+ file_path = os.path.join(output_dir, filename)
52
+
53
+ # Skip if not a file
54
+ if not os.path.isfile(file_path):
55
+ continue
56
+
57
+ # Skip annotated videos if keep_annotated is True
58
+ if keep_annotated and "_annotated" in filename:
59
+ continue
60
+
61
+ # Skip pro reference videos (they can be reused)
62
+ if "pro_reference" in filename:
63
+ continue
64
+
65
+ # Get file size before deletion
66
+ try:
67
+ file_size = os.path.getsize(file_path)
68
+ space_freed += file_size
69
+
70
+ # Remove the file
71
+ os.remove(file_path)
72
+ files_removed += 1
73
+ print(f"Cleaned up: {filename}")
74
+
75
+ except Exception as e:
76
+ print(f"Error removing {filename}: {str(e)}")
77
+
78
+ # Convert bytes to MB
79
+ space_freed_mb = space_freed / (1024 * 1024)
80
+
81
+ return {
82
+ "files_removed": files_removed,
83
+ "space_freed_mb": round(space_freed_mb, 2)
84
+ }
85
+
86
+ except Exception as e:
87
+ print(f"Error during cleanup: {str(e)}")
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
app/utils/video_processor.py CHANGED
@@ -20,13 +20,13 @@ class Detection:
20
  self.confidence = confidence
21
 
22
 
23
- def process_video(video_path, sample_rate=5):
24
  """
25
  Process video and detect golfer, club, and ball
26
 
27
  Args:
28
  video_path (str): Path to the video file
29
- sample_rate (int): Process every nth frame
30
 
31
  Returns:
32
  tuple: (frames, detections)
@@ -50,10 +50,7 @@ def process_video(video_path, sample_rate=5):
50
 
51
  frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
52
 
53
- if frame_count < 150:
54
- print(f"Short video detected ({frame_count} frames). Processing all frames.")
55
- sample_rate = 1
56
-
57
  frames = []
58
  detections = []
59
 
 
20
  self.confidence = confidence
21
 
22
 
23
+ def process_video(video_path, sample_rate=1):
24
  """
25
  Process video and detect golfer, club, and ball
26
 
27
  Args:
28
  video_path (str): Path to the video file
29
+ sample_rate (int): Process every nth frame (default: 1 for all frames)
30
 
31
  Returns:
32
  tuple: (frames, detections)
 
50
 
51
  frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
52
 
53
+ # Process all frames by default
 
 
 
54
  frames = []
55
  detections = []
56
 
app/utils/visualizer.py CHANGED
@@ -38,7 +38,7 @@ def create_annotated_video(video_path,
38
  swing_phases,
39
  trajectory_data,
40
  output_dir="downloads",
41
- sample_rate=5):
42
  """
43
  Create an annotated video with swing analysis visualizations
44
 
 
38
  swing_phases,
39
  trajectory_data,
40
  output_dir="downloads",
41
+ sample_rate=1):
42
  """
43
  Create an annotated video with swing analysis visualizations
44