Spaces:
Paused
Paused
comparison
Browse files- app/models/llm_analyzer.py +785 -305
app/models/llm_analyzer.py
CHANGED
|
@@ -7,6 +7,8 @@ import httpx
|
|
| 7 |
from openai import OpenAI
|
| 8 |
import streamlit as st
|
| 9 |
import re
|
|
|
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
def check_llm_services():
|
|
@@ -210,282 +212,425 @@ def call_openai_service(prompt, config):
|
|
| 210 |
return None
|
| 211 |
|
| 212 |
|
| 213 |
-
def prepare_data_for_llm(pose_data, swing_phases, trajectory_data):
|
| 214 |
"""
|
| 215 |
Prepare swing data for LLM analysis
|
| 216 |
|
| 217 |
Args:
|
| 218 |
pose_data (dict): Dictionary mapping frame indices to pose keypoints
|
| 219 |
swing_phases (dict): Dictionary mapping phase names to lists of frame indices
|
| 220 |
-
trajectory_data (dict):
|
| 221 |
|
| 222 |
Returns:
|
| 223 |
-
dict:
|
| 224 |
"""
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
# Get joint angles for the representative frame
|
| 234 |
-
if mid_frame in pose_data:
|
| 235 |
-
keypoints = pose_data[mid_frame]
|
| 236 |
-
|
| 237 |
-
# Calculate key metrics for each phase
|
| 238 |
-
analysis_data["swing_phases"][phase] = {
|
| 239 |
-
"frame_index": mid_frame,
|
| 240 |
-
"duration_frames": len(frames)
|
| 241 |
-
}
|
| 242 |
-
|
| 243 |
-
# Process trajectory data
|
| 244 |
-
impact_frames = swing_phases.get("impact", [])
|
| 245 |
-
if impact_frames:
|
| 246 |
-
impact_frame = impact_frames[len(impact_frames) // 2]
|
| 247 |
-
if impact_frame in trajectory_data:
|
| 248 |
-
impact_data = trajectory_data[impact_frame]
|
| 249 |
-
if "club_speed" in impact_data and impact_data["club_speed"]:
|
| 250 |
-
analysis_data["trajectory"]["club_speed_mph"] = impact_data[
|
| 251 |
-
"club_speed"]
|
| 252 |
-
|
| 253 |
-
# Calculate backswing and downswing durations if available
|
| 254 |
-
backswing_frames = swing_phases.get("backswing", [])
|
| 255 |
downswing_frames = swing_phases.get("downswing", [])
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
if backswing_frames
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
#
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
}
|
| 317 |
-
|
| 318 |
-
return
|
| 319 |
|
| 320 |
|
| 321 |
def create_llm_prompt(analysis_data):
|
| 322 |
"""
|
| 323 |
-
Create a prompt for
|
| 324 |
|
| 325 |
Args:
|
| 326 |
-
analysis_data (dict): Processed swing analysis data
|
| 327 |
|
| 328 |
Returns:
|
| 329 |
-
str:
|
| 330 |
"""
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
-
|
| 354 |
-
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
-
|
| 359 |
-
-
|
| 360 |
-
-
|
| 361 |
-
|
| 362 |
-
###
|
| 363 |
-
**
|
| 364 |
-
-
|
| 365 |
-
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
- **
|
| 371 |
-
- **
|
| 372 |
-
- **
|
| 373 |
-
- **Efficiency**:
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
|
| 393 |
# Core body mechanics
|
| 394 |
-
prompt += "\n### Body Mechanics\n"
|
| 395 |
-
prompt += "-
|
| 396 |
-
|
| 397 |
-
prompt += "-
|
| 398 |
-
|
| 399 |
-
prompt += "- Shoulder Rotation (degrees): {}\n".format(
|
| 400 |
-
analysis_data["metrics"].get("shoulder_rotation", 0))
|
| 401 |
-
prompt += "- Posture Score: {}%\n".format(
|
| 402 |
-
int(analysis_data["metrics"].get("posture_score", 0) * 100))
|
| 403 |
|
| 404 |
# Upper body mechanics
|
| 405 |
prompt += "\n### Upper Body Mechanics\n"
|
| 406 |
-
prompt += "- Arm Extension (
|
| 407 |
-
|
| 408 |
-
prompt += "-
|
| 409 |
-
|
| 410 |
-
prompt += "-
|
| 411 |
-
|
| 412 |
-
prompt += "- Chest Rotation Efficiency: {}%\n".format(
|
| 413 |
-
int(analysis_data["metrics"].get("chest_rotation_efficiency", 0.75) *
|
| 414 |
-
100))
|
| 415 |
-
prompt += "- Head Movement (lateral): {}in\n".format(
|
| 416 |
-
analysis_data["metrics"].get("head_movement_lateral", 2.5))
|
| 417 |
-
prompt += "- Head Movement (vertical): {}in\n".format(
|
| 418 |
-
analysis_data["metrics"].get("head_movement_vertical", 1.8))
|
| 419 |
|
| 420 |
# Lower body mechanics
|
| 421 |
prompt += "\n### Lower Body Mechanics\n"
|
| 422 |
-
prompt += "-
|
| 423 |
-
|
| 424 |
-
prompt += "-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
prompt += "
|
| 429 |
-
|
| 430 |
-
prompt += "-
|
| 431 |
-
int(analysis_data["metrics"].get("ground_force_efficiency", 0.7) *
|
| 432 |
-
100))
|
| 433 |
-
|
| 434 |
-
# Tempo and timing metrics
|
| 435 |
-
prompt += "\n### Tempo & Timing\n"
|
| 436 |
-
prompt += "- Transition Smoothness: {}%\n".format(
|
| 437 |
-
int(analysis_data["metrics"].get("transition_smoothness", 0.75) * 100))
|
| 438 |
-
prompt += "- Backswing Duration: {} seconds\n".format(
|
| 439 |
-
analysis_data["metrics"].get("backswing_duration", 0.9))
|
| 440 |
-
prompt += "- Downswing Duration: {} seconds\n".format(
|
| 441 |
-
analysis_data["metrics"].get("downswing_duration", 0.3))
|
| 442 |
-
prompt += "- Sequential Kinematic Sequence: {}%\n".format(
|
| 443 |
-
int(analysis_data["metrics"].get("kinematic_sequence", 0.82) * 100))
|
| 444 |
|
| 445 |
# Efficiency and power metrics
|
| 446 |
prompt += "\n### Efficiency & Power Metrics\n"
|
| 447 |
-
prompt += "- Energy Transfer Efficiency: {}%\n"
|
| 448 |
-
|
| 449 |
-
prompt += "- Potential Distance: {} yards\n"
|
| 450 |
-
|
| 451 |
-
prompt += "- Power Accumulation: {}%\n".format(
|
| 452 |
-
int(analysis_data["metrics"].get("power_accumulation", 0.75) * 100))
|
| 453 |
-
prompt += "- Speed Generation Method: {}\n".format(
|
| 454 |
-
analysis_data["metrics"].get("speed_generation", "Arms-dominant"))
|
| 455 |
|
| 456 |
prompt += """
|
| 457 |
|
| 458 |
## ANALYSIS INSTRUCTIONS
|
| 459 |
|
| 460 |
-
Using the professional benchmarks above as your calibration reference, provide your analysis in the following EXACT structured format:
|
| 461 |
|
| 462 |
-
**PERFORMANCE_CLASSIFICATION:** [
|
| 463 |
|
| 464 |
**STRENGTHS:**
|
| 465 |
-
• [Specific strength with
|
| 466 |
-
• [Another strength with professional
|
| 467 |
-
• [Third strength
|
| 468 |
|
| 469 |
**WEAKNESSES:**
|
| 470 |
-
• [Specific weakness with gap from professional standard]
|
| 471 |
-
• [Another weakness with professional
|
| 472 |
-
• [Third weakness
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
|
| 474 |
**PRIORITY_IMPROVEMENTS:**
|
| 475 |
-
1. [Most Critical]
|
| 476 |
-
2. [Important]
|
| 477 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
|
| 479 |
IMPORTANT FORMATTING RULES:
|
| 480 |
- Use the exact headers shown above (PERFORMANCE_CLASSIFICATION, STRENGTHS, WEAKNESSES, PRIORITY_IMPROVEMENTS)
|
|
|
|
| 481 |
- For strengths and weaknesses, use bullet points (•)
|
| 482 |
- For priority improvements, use numbered format (1., 2., 3.) with priority level in brackets
|
| 483 |
-
- Each priority improvement must have: [Priority Level] Topic Name - Full description
|
| 484 |
-
-
|
| 485 |
-
-
|
| 486 |
-
-
|
| 487 |
-
|
| 488 |
-
|
|
|
|
|
|
|
| 489 |
"""
|
| 490 |
|
| 491 |
return prompt
|
|
@@ -503,29 +648,32 @@ def parse_and_format_analysis(raw_analysis):
|
|
| 503 |
"""
|
| 504 |
# Default structure
|
| 505 |
formatted_analysis = {
|
| 506 |
-
'classification':
|
| 507 |
'strengths': [],
|
| 508 |
'weaknesses': [],
|
| 509 |
'priority_improvements': []
|
| 510 |
}
|
| 511 |
|
| 512 |
-
# Extract classification using the new structured format
|
| 513 |
-
classification_match = re.search(r'\*\*PERFORMANCE_CLASSIFICATION:\*\*\s
|
| 514 |
if classification_match:
|
| 515 |
-
|
|
|
|
|
|
|
| 516 |
else:
|
| 517 |
-
# Fallback to
|
| 518 |
-
|
| 519 |
-
r'(?:Performance
|
| 520 |
-
r'(
|
| 521 |
-
r'classified
|
| 522 |
-
r'(?:at|as)\s+(
|
| 523 |
]
|
| 524 |
|
| 525 |
-
for pattern in
|
| 526 |
match = re.search(pattern, raw_analysis, re.IGNORECASE)
|
| 527 |
if match:
|
| 528 |
-
|
|
|
|
| 529 |
break
|
| 530 |
|
| 531 |
# Extract strengths using the new structured format
|
|
@@ -632,29 +780,30 @@ def parse_and_format_analysis(raw_analysis):
|
|
| 632 |
formatted_analysis['weaknesses'] = ['Areas for improvement identified']
|
| 633 |
|
| 634 |
if not formatted_analysis['priority_improvements']:
|
| 635 |
-
|
|
|
|
| 636 |
formatted_analysis['priority_improvements'] = [
|
| 637 |
-
{'rank': 1, 'description': '[Most Critical]
|
| 638 |
-
{'rank': 2, 'description': '[Important]
|
| 639 |
-
{'rank': 3, 'description': '[Focus Area]
|
| 640 |
]
|
| 641 |
-
elif
|
| 642 |
formatted_analysis['priority_improvements'] = [
|
| 643 |
{'rank': 1, 'description': '[Most Critical] Kinematic Sequence Enhancement - Improve body rotation coordination to generate more power and consistency.'},
|
| 644 |
{'rank': 2, 'description': '[Important] Clubface Control - Enhance swing path consistency for better ball striking accuracy.'},
|
| 645 |
{'rank': 3, 'description': '[Focus Area] Energy Transfer Efficiency - Optimize power transfer throughout the swing to maximize distance.'}
|
| 646 |
]
|
| 647 |
-
elif
|
| 648 |
formatted_analysis['priority_improvements'] = [
|
| 649 |
-
{'rank': 1, 'description': '[Most Critical]
|
| 650 |
-
{'rank': 2, 'description': '[Important]
|
| 651 |
-
{'rank': 3, 'description': '[Focus Area]
|
| 652 |
]
|
| 653 |
-
else: #
|
| 654 |
formatted_analysis['priority_improvements'] = [
|
| 655 |
-
{'rank': 1, 'description': '[Most Critical]
|
| 656 |
-
{'rank': 2, 'description': '[Important]
|
| 657 |
-
{'rank': 3, 'description': '[Focus Area]
|
| 658 |
]
|
| 659 |
|
| 660 |
return formatted_analysis
|
|
@@ -667,70 +816,89 @@ def display_formatted_analysis(analysis_data):
|
|
| 667 |
Args:
|
| 668 |
analysis_data (dict): Structured analysis data from parse_and_format_analysis
|
| 669 |
"""
|
| 670 |
-
# 1. Performance Classification with
|
| 671 |
-
|
| 672 |
|
| 673 |
# Display classification in black bolded header
|
| 674 |
st.markdown(f"""
|
| 675 |
<h2 style='color: black; font-weight: bold; text-align: center; margin-bottom: 20px;'>
|
| 676 |
-
🎯 Performance
|
| 677 |
</h2>
|
| 678 |
""", unsafe_allow_html=True)
|
| 679 |
|
| 680 |
-
# Create
|
| 681 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 682 |
|
| 683 |
-
#
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
'
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
with col1:
|
| 692 |
-
bg_color = colors['Beginner']['bg']
|
| 693 |
-
text_color = colors['Beginner']['text']
|
| 694 |
-
border_style = '3px solid #333' if user_classification == 'Beginner' else '2px solid #ddd'
|
| 695 |
-
st.markdown(f"""
|
| 696 |
-
<div style='text-align: center; padding: 15px; background-color: {bg_color};
|
| 697 |
-
border-radius: 15px; margin: 5px; border: {border_style};'>
|
| 698 |
-
<div style='font-size: 14px; font-weight: bold; color: {text_color};'>Beginner</div>
|
| 699 |
</div>
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
border_style = '3px solid #333' if user_classification == 'Intermediate' else '2px solid #ddd'
|
| 706 |
-
st.markdown(f"""
|
| 707 |
-
<div style='text-align: center; padding: 15px; background-color: {bg_color};
|
| 708 |
-
border-radius: 15px; margin: 5px; border: {border_style};'>
|
| 709 |
-
<div style='font-size: 14px; font-weight: bold; color: {text_color};'>Intermediate</div>
|
| 710 |
</div>
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
<
|
|
|
|
| 721 |
</div>
|
| 722 |
-
|
|
|
|
| 723 |
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 734 |
|
| 735 |
st.markdown("---")
|
| 736 |
|
|
@@ -844,3 +1012,315 @@ def display_formatted_analysis(analysis_data):
|
|
| 844 |
st.write(desc)
|
| 845 |
|
| 846 |
st.write("") # Add spacing between items
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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():
|
|
|
|
| 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
|
|
|
|
| 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
|
|
|
|
| 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
|
|
|
|
| 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 |
|
|
|
|
| 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
|