Spaces:
Paused
Paused
style
Browse files- app/models/llm_analyzer.py +398 -12
- app/streamlit_app.py +8 -24
app/models/llm_analyzer.py
CHANGED
|
@@ -6,6 +6,7 @@ import json
|
|
| 6 |
import httpx
|
| 7 |
from openai import OpenAI
|
| 8 |
import streamlit as st
|
|
|
|
| 9 |
|
| 10 |
|
| 11 |
def check_llm_services():
|
|
@@ -328,6 +329,55 @@ def create_llm_prompt(analysis_data):
|
|
| 328 |
str: Prompt for LLM
|
| 329 |
"""
|
| 330 |
prompt = """
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 331 |
I've analyzed a golf swing and extracted the following data:
|
| 332 |
|
| 333 |
## Swing Phases
|
|
@@ -405,22 +455,358 @@ I've analyzed a golf swing and extracted the following data:
|
|
| 405 |
|
| 406 |
prompt += """
|
| 407 |
|
| 408 |
-
|
|
|
|
|
|
|
| 409 |
|
| 410 |
-
1.
|
| 411 |
-
- Detailed breakdown of each swing phase
|
| 412 |
-
- Analysis of body mechanics and kinematic sequence
|
| 413 |
-
- Assessment of power generation and efficiency
|
| 414 |
-
- Evaluation of clubface control and swing path
|
| 415 |
|
| 416 |
-
2.
|
| 417 |
-
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
|
|
|
|
|
|
|
|
|
| 421 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
|
| 423 |
-
|
|
|
|
|
|
|
| 424 |
"""
|
| 425 |
|
| 426 |
return prompt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
import httpx
|
| 7 |
from openai import OpenAI
|
| 8 |
import streamlit as st
|
| 9 |
+
import re
|
| 10 |
|
| 11 |
|
| 12 |
def check_llm_services():
|
|
|
|
| 329 |
str: Prompt for LLM
|
| 330 |
"""
|
| 331 |
prompt = """
|
| 332 |
+
You are analyzing a golf swing. First, here are examples of professional golfer swing analyses that represent benchmark performance levels:
|
| 333 |
+
|
| 334 |
+
## PROFESSIONAL BENCHMARKS
|
| 335 |
+
|
| 336 |
+
### Nelly Korda (Example 1) - LPGA Tour Professional
|
| 337 |
+
**Swing Phases:**
|
| 338 |
+
- Setup: 122 frames, Backswing: 2 frames, Downswing: 5 frames, Impact: 1 frame, Follow-through: 42 frames
|
| 339 |
+
|
| 340 |
+
**Key Metrics:**
|
| 341 |
+
- Tempo Ratio: 0.4, Hip Rotation: 45°, Shoulder Rotation: 90°, Posture Score: 80%
|
| 342 |
+
- Arm Extension: 80%, Wrist Hinge: 80°, Shoulder Plane Consistency: 85%
|
| 343 |
+
- Weight Shift: 70%, Transition Smoothness: 75%, Sequential Kinematic Sequence: 82%
|
| 344 |
+
- Energy Transfer Efficiency: 78%, Backswing Duration: 0.067s, Downswing Duration: 0.167s
|
| 345 |
+
|
| 346 |
+
### Nelly Korda (Example 2) - Different Swing Tempo Style
|
| 347 |
+
**Key Metrics:**
|
| 348 |
+
- Tempo Ratio: 3.0, Backswing Duration: 0.9s, Downswing Duration: 0.9s
|
| 349 |
+
- All other core metrics remain consistent: Hip Rotation: 45°, Shoulder Rotation: 90°, etc.
|
| 350 |
+
|
| 351 |
+
### Nelly Korda (Example 3) - Fast Tempo Style
|
| 352 |
+
**Key Metrics:**
|
| 353 |
+
- Tempo Ratio: 0.3, Backswing Duration: 0.067s, Downswing Duration: 0.2s
|
| 354 |
+
- Consistent professional metrics maintained across all mechanical aspects
|
| 355 |
+
|
| 356 |
+
### Lydia Ko - LPGA Tour Professional
|
| 357 |
+
**Key Metrics:**
|
| 358 |
+
- Tempo Ratio: 14.0, Backswing Duration: 0.467s, Downswing Duration: 0.033s
|
| 359 |
+
- Demonstrates that professional tempo can vary dramatically while maintaining consistency in:
|
| 360 |
+
- Hip/Shoulder Rotation, Posture, Arm Extension, Weight Shift, and Sequential Timing
|
| 361 |
+
|
| 362 |
+
### Atthaya Thitikul - LPGA Tour Professional
|
| 363 |
+
**Key Metrics:**
|
| 364 |
+
- Tempo Ratio: 2.8, Backswing Duration: 0.567s, Downswing Duration: 0.2s
|
| 365 |
+
- Consistent with professional standards across all biomechanical markers
|
| 366 |
+
|
| 367 |
+
## PROFESSIONAL STANDARDS SUMMARY
|
| 368 |
+
Based on these examples, professional golfers consistently achieve:
|
| 369 |
+
- **Core Body Mechanics**: Hip Rotation: 45°, Shoulder Rotation: 90°, Posture Score: 80%
|
| 370 |
+
- **Upper Body**: Arm Extension: 80%, Wrist Hinge: 80°, Shoulder Plane Consistency: 85%
|
| 371 |
+
- **Lower Body**: Weight Shift: 70%, Ground Force Efficiency: 70%
|
| 372 |
+
- **Timing**: Transition Smoothness: 75%, Sequential Kinematic Sequence: 82%
|
| 373 |
+
- **Efficiency**: Energy Transfer: 78%, Power Accumulation: 75%
|
| 374 |
+
- **Head Movement**: Lateral: 2.5in, Vertical: 1.8in (minimal movement is professional standard)
|
| 375 |
+
- **Tempo**: Highly variable (0.3 to 14.0 ratio) - personal style, not performance indicator
|
| 376 |
+
|
| 377 |
+
---
|
| 378 |
+
|
| 379 |
+
## CURRENT PLAYER ANALYSIS
|
| 380 |
+
|
| 381 |
I've analyzed a golf swing and extracted the following data:
|
| 382 |
|
| 383 |
## Swing Phases
|
|
|
|
| 455 |
|
| 456 |
prompt += """
|
| 457 |
|
| 458 |
+
## ANALYSIS INSTRUCTIONS
|
| 459 |
+
|
| 460 |
+
Using the professional benchmarks above as your calibration reference, provide:
|
| 461 |
|
| 462 |
+
1. **Performance Classification**: Start with "Performance Classification: [Professional/Advanced/Intermediate/Beginner]" based on how the player's metrics compare to professional standards.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
|
| 464 |
+
2. **Comparative Analysis**:
|
| 465 |
+
- **Strengths** (metrics that meet/exceed professional benchmarks):
|
| 466 |
+
• List specific strong points (use bullet points)
|
| 467 |
+
• Reference professional benchmark values
|
| 468 |
+
|
| 469 |
+
- **Areas for Improvement** (metrics significantly below professional standards):
|
| 470 |
+
• List specific weaknesses (use bullet points)
|
| 471 |
+
• Note the gap from professional standards
|
| 472 |
|
| 473 |
+
3. **Priority Improvement Areas**: List exactly 3 areas in order of importance:
|
| 474 |
+
1. [Most Critical] - Describe what's wrong and what it should be like
|
| 475 |
+
2. [Important] - Describe what's wrong and what it should be like
|
| 476 |
+
3. [Focus Area] - Describe what's wrong and what it should be like
|
| 477 |
|
| 478 |
+
Remember: Professional golfers consistently achieve the benchmark metrics shown above. Use these as the gold standard for what constitutes excellent golf swing mechanics, while being realistic about the progression needed to reach those levels.
|
| 479 |
+
|
| 480 |
+
Provide your analysis in the structured format above for optimal coaching feedback.
|
| 481 |
"""
|
| 482 |
|
| 483 |
return prompt
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
def parse_and_format_analysis(raw_analysis):
|
| 487 |
+
"""
|
| 488 |
+
Parse the raw LLM analysis and format it into structured components
|
| 489 |
+
|
| 490 |
+
Args:
|
| 491 |
+
raw_analysis (str): Raw analysis text from LLM
|
| 492 |
+
|
| 493 |
+
Returns:
|
| 494 |
+
dict: Structured analysis with classification, strengths/weaknesses, and priorities
|
| 495 |
+
"""
|
| 496 |
+
# Default structure
|
| 497 |
+
formatted_analysis = {
|
| 498 |
+
'classification': 'Intermediate', # Default classification
|
| 499 |
+
'strengths': [],
|
| 500 |
+
'weaknesses': [],
|
| 501 |
+
'priority_improvements': []
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
# Try to extract classification from the analysis
|
| 505 |
+
classification_patterns = [
|
| 506 |
+
r'(?:Performance Classification|Classification|Level).*?:\s*(Professional|Advanced|Intermediate|Beginner)',
|
| 507 |
+
r'(Professional|Advanced|Intermediate|Beginner)\s+(?:Level|Amateur)',
|
| 508 |
+
r'classified as\s+(Professional|Advanced|Intermediate|Beginner)',
|
| 509 |
+
r'(?:at|as)\s+(?:an?\s+)?(Professional|Advanced|Intermediate|Beginner)\s+level'
|
| 510 |
+
]
|
| 511 |
+
|
| 512 |
+
classification_found = False
|
| 513 |
+
for pattern in classification_patterns:
|
| 514 |
+
match = re.search(pattern, raw_analysis, re.IGNORECASE)
|
| 515 |
+
if match:
|
| 516 |
+
formatted_analysis['classification'] = match.group(1).title()
|
| 517 |
+
classification_found = True
|
| 518 |
+
break
|
| 519 |
+
|
| 520 |
+
# If no classification found, try to infer from content
|
| 521 |
+
if not classification_found:
|
| 522 |
+
analysis_lower = raw_analysis.lower()
|
| 523 |
+
if 'professional' in analysis_lower and ('meets' in analysis_lower or 'exceeds' in analysis_lower):
|
| 524 |
+
formatted_analysis['classification'] = 'Professional'
|
| 525 |
+
elif 'advanced' in analysis_lower or ('within 10' in analysis_lower and 'pro' in analysis_lower):
|
| 526 |
+
formatted_analysis['classification'] = 'Advanced'
|
| 527 |
+
elif 'beginner' in analysis_lower or ('30%' in analysis_lower and 'below' in analysis_lower):
|
| 528 |
+
formatted_analysis['classification'] = 'Beginner'
|
| 529 |
+
else:
|
| 530 |
+
formatted_analysis['classification'] = 'Intermediate'
|
| 531 |
+
|
| 532 |
+
# Extract strengths and weaknesses
|
| 533 |
+
strengths_section = ""
|
| 534 |
+
weaknesses_section = ""
|
| 535 |
+
|
| 536 |
+
# Look for strengths/weaknesses sections
|
| 537 |
+
strengths_patterns = [
|
| 538 |
+
r'(?:Strengths|Strong Points|Positives|Meets.*Standards)[\s\S]*?(?=(?:Weak|Priority|Improvement|Areas|$))',
|
| 539 |
+
r'(?:Professional Level|Exceeds.*Standards)[\s\S]*?(?=(?:Below|Weak|Priority|$))'
|
| 540 |
+
]
|
| 541 |
+
|
| 542 |
+
weaknesses_patterns = [
|
| 543 |
+
r'(?:Weaknesses|Weak|Areas.*Improvement|Priority.*Areas|Below.*Standards)[\s\S]*?(?=(?:Recommendation|Priority|$))',
|
| 544 |
+
r'(?:Critical|Important|Significant.*gaps?)[\s\S]*?(?=(?:Recommendation|$))'
|
| 545 |
+
]
|
| 546 |
+
|
| 547 |
+
for pattern in strengths_patterns:
|
| 548 |
+
match = re.search(pattern, raw_analysis, re.IGNORECASE)
|
| 549 |
+
if match:
|
| 550 |
+
strengths_section = match.group(0)
|
| 551 |
+
break
|
| 552 |
+
|
| 553 |
+
for pattern in weaknesses_patterns:
|
| 554 |
+
match = re.search(pattern, raw_analysis, re.IGNORECASE)
|
| 555 |
+
if match:
|
| 556 |
+
weaknesses_section = match.group(0)
|
| 557 |
+
break
|
| 558 |
+
|
| 559 |
+
# Parse strengths from the section
|
| 560 |
+
if strengths_section:
|
| 561 |
+
strength_items = re.findall(r'[-•]\s*([^-•\n]+)', strengths_section)
|
| 562 |
+
formatted_analysis['strengths'] = [item.strip() for item in strength_items[:4]] # Limit to 4
|
| 563 |
+
|
| 564 |
+
# If no bullet points found, try to extract from general content
|
| 565 |
+
if not formatted_analysis['strengths']:
|
| 566 |
+
# Look for positive indicators in the full text
|
| 567 |
+
positive_indicators = [
|
| 568 |
+
r'(?:meets|exceeds|matches).*professional.*(?:standard|benchmark)',
|
| 569 |
+
r'(?:excellent|good|strong).*(?:posture|rotation|extension|timing)',
|
| 570 |
+
r'(?:consistent|solid).*(?:mechanics|form|technique)',
|
| 571 |
+
r'(?:efficient|effective).*(?:transfer|generation|sequence)'
|
| 572 |
+
]
|
| 573 |
+
|
| 574 |
+
for pattern in positive_indicators:
|
| 575 |
+
matches = re.findall(pattern, raw_analysis, re.IGNORECASE)
|
| 576 |
+
for match in matches[:2]: # Limit to avoid overwhelming
|
| 577 |
+
formatted_analysis['strengths'].append(match.strip())
|
| 578 |
+
|
| 579 |
+
# Parse weaknesses from the section
|
| 580 |
+
if weaknesses_section:
|
| 581 |
+
weakness_items = re.findall(r'[-•]\s*([^-•\n]+)', weaknesses_section)
|
| 582 |
+
formatted_analysis['weaknesses'] = [item.strip() for item in weakness_items[:4]] # Limit to 4
|
| 583 |
+
|
| 584 |
+
# If no bullet points found, try to extract from general content
|
| 585 |
+
if not formatted_analysis['weaknesses']:
|
| 586 |
+
# Look for negative indicators in the full text
|
| 587 |
+
negative_indicators = [
|
| 588 |
+
r'(?:below|under).*professional.*(?:standard|benchmark)',
|
| 589 |
+
r'(?:poor|weak|limited).*(?:posture|rotation|extension|timing)',
|
| 590 |
+
r'(?:inconsistent|unstable).*(?:mechanics|form|technique)',
|
| 591 |
+
r'(?:inefficient|ineffective).*(?:transfer|generation|sequence)'
|
| 592 |
+
]
|
| 593 |
+
|
| 594 |
+
for pattern in negative_indicators:
|
| 595 |
+
matches = re.findall(pattern, raw_analysis, re.IGNORECASE)
|
| 596 |
+
for match in matches[:2]: # Limit to avoid overwhelming
|
| 597 |
+
formatted_analysis['weaknesses'].append(match.strip())
|
| 598 |
+
|
| 599 |
+
# Extract priority improvements
|
| 600 |
+
priority_patterns = [
|
| 601 |
+
r'(?:Priority.*Improvement|Critical.*Areas?)[\s\S]*?(?=(?:Recommendation|$))',
|
| 602 |
+
r'(?:1\..*?2\..*?3\.)', # Numbered list
|
| 603 |
+
r'(?:Critical|Important|Fine-tuning)[\s\S]*?(?=(?:Critical|Important|Fine-tuning|$))'
|
| 604 |
+
]
|
| 605 |
+
|
| 606 |
+
for pattern in priority_patterns:
|
| 607 |
+
match = re.search(pattern, raw_analysis, re.IGNORECASE | re.DOTALL)
|
| 608 |
+
if match:
|
| 609 |
+
priority_text = match.group(0)
|
| 610 |
+
# Extract numbered items
|
| 611 |
+
numbered_items = re.findall(r'(\d+)\.\s*([^1-9\n]+)', priority_text)
|
| 612 |
+
for num, item in numbered_items[:3]: # Limit to 3
|
| 613 |
+
formatted_analysis['priority_improvements'].append({
|
| 614 |
+
'rank': int(num),
|
| 615 |
+
'description': item.strip()
|
| 616 |
+
})
|
| 617 |
+
break
|
| 618 |
+
|
| 619 |
+
# If no numbered priorities found, create generic ones based on classification
|
| 620 |
+
if not formatted_analysis['priority_improvements']:
|
| 621 |
+
if formatted_analysis['classification'] == 'Beginner':
|
| 622 |
+
formatted_analysis['priority_improvements'] = [
|
| 623 |
+
{'rank': 1, 'description': 'Focus on fundamental posture and setup position'},
|
| 624 |
+
{'rank': 2, 'description': 'Develop consistent tempo and timing'},
|
| 625 |
+
{'rank': 3, 'description': 'Improve weight shift and balance throughout swing'}
|
| 626 |
+
]
|
| 627 |
+
elif formatted_analysis['classification'] == 'Intermediate':
|
| 628 |
+
formatted_analysis['priority_improvements'] = [
|
| 629 |
+
{'rank': 1, 'description': 'Enhance kinematic sequence and body rotation'},
|
| 630 |
+
{'rank': 2, 'description': 'Improve clubface control and swing path consistency'},
|
| 631 |
+
{'rank': 3, 'description': 'Optimize energy transfer efficiency'}
|
| 632 |
+
]
|
| 633 |
+
elif formatted_analysis['classification'] == 'Advanced':
|
| 634 |
+
formatted_analysis['priority_improvements'] = [
|
| 635 |
+
{'rank': 1, 'description': 'Fine-tune transition smoothness and timing'},
|
| 636 |
+
{'rank': 2, 'description': 'Optimize power accumulation and release'},
|
| 637 |
+
{'rank': 3, 'description': 'Enhance consistency under pressure'}
|
| 638 |
+
]
|
| 639 |
+
else: # Professional
|
| 640 |
+
formatted_analysis['priority_improvements'] = [
|
| 641 |
+
{'rank': 1, 'description': 'Maintain current excellence with minor adjustments'},
|
| 642 |
+
{'rank': 2, 'description': 'Focus on course management and strategy'},
|
| 643 |
+
{'rank': 3, 'description': 'Continue physical conditioning for longevity'}
|
| 644 |
+
]
|
| 645 |
+
|
| 646 |
+
# Ensure we have some default content if parsing failed
|
| 647 |
+
if not formatted_analysis['strengths']:
|
| 648 |
+
formatted_analysis['strengths'] = ['Swing analysis completed successfully']
|
| 649 |
+
|
| 650 |
+
if not formatted_analysis['weaknesses']:
|
| 651 |
+
formatted_analysis['weaknesses'] = ['Areas for improvement identified']
|
| 652 |
+
|
| 653 |
+
return formatted_analysis
|
| 654 |
+
|
| 655 |
+
|
| 656 |
+
def display_formatted_analysis(analysis_data):
|
| 657 |
+
"""
|
| 658 |
+
Display the formatted analysis with performance classification, strengths/weaknesses table, and priorities
|
| 659 |
+
|
| 660 |
+
Args:
|
| 661 |
+
analysis_data (dict): Structured analysis data from parse_and_format_analysis
|
| 662 |
+
"""
|
| 663 |
+
# 1. Performance Classification with colored rounded rectangles
|
| 664 |
+
user_classification = analysis_data['classification']
|
| 665 |
+
|
| 666 |
+
# Display classification in black bolded header
|
| 667 |
+
st.markdown(f"""
|
| 668 |
+
<h2 style='color: black; font-weight: bold; text-align: center; margin-bottom: 20px;'>
|
| 669 |
+
🎯 Performance Classification: {user_classification}
|
| 670 |
+
</h2>
|
| 671 |
+
""", unsafe_allow_html=True)
|
| 672 |
+
|
| 673 |
+
# Create columns for the classification rectangles
|
| 674 |
+
col1, col2, col3, col4 = st.columns(4)
|
| 675 |
+
|
| 676 |
+
# Define colors and styling - all rectangles should have colors
|
| 677 |
+
colors = {
|
| 678 |
+
'Beginner': {'bg': '#ff4444', 'text': 'white'},
|
| 679 |
+
'Intermediate': {'bg': '#ff8800', 'text': 'white'},
|
| 680 |
+
'Advanced': {'bg': '#ffdd00', 'text': 'black'},
|
| 681 |
+
'Professional': {'bg': '#44aa44', 'text': 'white'}
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
with col1:
|
| 685 |
+
bg_color = colors['Beginner']['bg']
|
| 686 |
+
text_color = colors['Beginner']['text']
|
| 687 |
+
border_style = '3px solid #333' if user_classification == 'Beginner' else '2px solid #ddd'
|
| 688 |
+
st.markdown(f"""
|
| 689 |
+
<div style='text-align: center; padding: 15px; background-color: {bg_color};
|
| 690 |
+
border-radius: 15px; margin: 5px; border: {border_style};'>
|
| 691 |
+
<div style='font-size: 14px; font-weight: bold; color: {text_color};'>Beginner</div>
|
| 692 |
+
</div>
|
| 693 |
+
""", unsafe_allow_html=True)
|
| 694 |
+
|
| 695 |
+
with col2:
|
| 696 |
+
bg_color = colors['Intermediate']['bg']
|
| 697 |
+
text_color = colors['Intermediate']['text']
|
| 698 |
+
border_style = '3px solid #333' if user_classification == 'Intermediate' else '2px solid #ddd'
|
| 699 |
+
st.markdown(f"""
|
| 700 |
+
<div style='text-align: center; padding: 15px; background-color: {bg_color};
|
| 701 |
+
border-radius: 15px; margin: 5px; border: {border_style};'>
|
| 702 |
+
<div style='font-size: 14px; font-weight: bold; color: {text_color};'>Intermediate</div>
|
| 703 |
+
</div>
|
| 704 |
+
""", unsafe_allow_html=True)
|
| 705 |
+
|
| 706 |
+
with col3:
|
| 707 |
+
bg_color = colors['Advanced']['bg']
|
| 708 |
+
text_color = colors['Advanced']['text']
|
| 709 |
+
border_style = '3px solid #333' if user_classification == 'Advanced' else '2px solid #ddd'
|
| 710 |
+
st.markdown(f"""
|
| 711 |
+
<div style='text-align: center; padding: 15px; background-color: {bg_color};
|
| 712 |
+
border-radius: 15px; margin: 5px; border: {border_style};'>
|
| 713 |
+
<div style='font-size: 14px; font-weight: bold; color: {text_color};'>Advanced</div>
|
| 714 |
+
</div>
|
| 715 |
+
""", unsafe_allow_html=True)
|
| 716 |
+
|
| 717 |
+
with col4:
|
| 718 |
+
bg_color = colors['Professional']['bg']
|
| 719 |
+
text_color = colors['Professional']['text']
|
| 720 |
+
border_style = '3px solid #333' if user_classification == 'Professional' else '2px solid #ddd'
|
| 721 |
+
st.markdown(f"""
|
| 722 |
+
<div style='text-align: center; padding: 15px; background-color: {bg_color};
|
| 723 |
+
border-radius: 15px; margin: 5px; border: {border_style};'>
|
| 724 |
+
<div style='font-size: 14px; font-weight: bold; color: {text_color};'>Professional</div>
|
| 725 |
+
</div>
|
| 726 |
+
""", unsafe_allow_html=True)
|
| 727 |
+
|
| 728 |
+
st.markdown("---")
|
| 729 |
+
|
| 730 |
+
# 2. Strengths and Weaknesses Table
|
| 731 |
+
st.subheader("⚖️ Strengths & Areas for Improvement")
|
| 732 |
+
|
| 733 |
+
# Create two columns for the table with a visual divider
|
| 734 |
+
col_left, col_divider, col_right = st.columns([5, 1, 5])
|
| 735 |
+
|
| 736 |
+
with col_left:
|
| 737 |
+
st.markdown("""
|
| 738 |
+
<div style='background-color: #e8f5e8; padding: 15px; border-radius: 10px; height: 100%;'>
|
| 739 |
+
<h4 style='color: #2d5a2d; margin-top: 0;'>✅ Strengths</h4>
|
| 740 |
+
""", unsafe_allow_html=True)
|
| 741 |
+
for strength in analysis_data['strengths']:
|
| 742 |
+
st.markdown(f"• {strength}")
|
| 743 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 744 |
+
|
| 745 |
+
with col_divider:
|
| 746 |
+
st.markdown("""
|
| 747 |
+
<div style='width: 2px; background-color: #ddd; height: 200px; margin: 20px auto;'></div>
|
| 748 |
+
""", unsafe_allow_html=True)
|
| 749 |
+
|
| 750 |
+
with col_right:
|
| 751 |
+
st.markdown("""
|
| 752 |
+
<div style='background-color: #fff5e6; padding: 15px; border-radius: 10px; height: 100%;'>
|
| 753 |
+
<h4 style='color: #cc6600; margin-top: 0;'>⚠️ Areas for Improvement</h4>
|
| 754 |
+
""", unsafe_allow_html=True)
|
| 755 |
+
for weakness in analysis_data['weaknesses']:
|
| 756 |
+
st.markdown(f"• {weakness}")
|
| 757 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
| 758 |
+
|
| 759 |
+
st.markdown("---")
|
| 760 |
+
|
| 761 |
+
# 3. Priority Improvement Areas
|
| 762 |
+
st.subheader("🎯 Priority Improvement Areas")
|
| 763 |
+
|
| 764 |
+
for priority in sorted(analysis_data['priority_improvements'], key=lambda x: x['rank']):
|
| 765 |
+
rank = priority['rank']
|
| 766 |
+
description = priority['description']
|
| 767 |
+
|
| 768 |
+
# Extract improvement area and description if possible
|
| 769 |
+
if ':' in description:
|
| 770 |
+
area, desc = description.split(':', 1)
|
| 771 |
+
area = area.strip()
|
| 772 |
+
desc = desc.strip()
|
| 773 |
+
elif '-' in description:
|
| 774 |
+
parts = description.split('-', 1)
|
| 775 |
+
if len(parts) == 2:
|
| 776 |
+
area = parts[0].strip()
|
| 777 |
+
desc = parts[1].strip()
|
| 778 |
+
else:
|
| 779 |
+
area = description
|
| 780 |
+
desc = ""
|
| 781 |
+
else:
|
| 782 |
+
# Try to extract first sentence as area, rest as description
|
| 783 |
+
sentences = description.split('. ')
|
| 784 |
+
if len(sentences) > 1:
|
| 785 |
+
area = sentences[0]
|
| 786 |
+
desc = '. '.join(sentences[1:])
|
| 787 |
+
else:
|
| 788 |
+
area = description
|
| 789 |
+
desc = ""
|
| 790 |
+
|
| 791 |
+
# Color code by priority with better styling
|
| 792 |
+
if rank == 1:
|
| 793 |
+
st.markdown(f"""
|
| 794 |
+
<div style='background-color: #ffebee; padding: 15px; border-left: 5px solid #f44336; border-radius: 5px; margin: 10px 0; word-wrap: break-word; overflow-wrap: break-word;'>
|
| 795 |
+
<strong style='color: #d32f2f; font-size: 16px; display: block; margin-bottom: 8px;'>{rank}. MOST CRITICAL: {area}</strong>
|
| 796 |
+
{f"<div style='color: #666; font-size: 14px; line-height: 1.4; word-wrap: break-word;'>{desc}</div>" if desc else ""}
|
| 797 |
+
</div>
|
| 798 |
+
""", unsafe_allow_html=True)
|
| 799 |
+
elif rank == 2:
|
| 800 |
+
st.markdown(f"""
|
| 801 |
+
<div style='background-color: #fff8e1; padding: 15px; border-left: 5px solid #ff9800; border-radius: 5px; margin: 10px 0; word-wrap: break-word; overflow-wrap: break-word;'>
|
| 802 |
+
<strong style='color: #f57c00; font-size: 16px; display: block; margin-bottom: 8px;'>{rank}. IMPORTANT: {area}</strong>
|
| 803 |
+
{f"<div style='color: #666; font-size: 14px; line-height: 1.4; word-wrap: break-word;'>{desc}</div>" if desc else ""}
|
| 804 |
+
</div>
|
| 805 |
+
""", unsafe_allow_html=True)
|
| 806 |
+
else:
|
| 807 |
+
st.markdown(f"""
|
| 808 |
+
<div style='background-color: #e3f2fd; padding: 15px; border-left: 5px solid #2196f3; border-radius: 5px; margin: 10px 0; word-wrap: break-word; overflow-wrap: break-word;'>
|
| 809 |
+
<strong style='color: #1976d2; font-size: 16px; display: block; margin-bottom: 8px;'>{rank}. FOCUS AREA: {area}</strong>
|
| 810 |
+
{f"<div style='color: #666; font-size: 14px; line-height: 1.4; word-wrap: break-word;'>{desc}</div>" if desc else ""}
|
| 811 |
+
</div>
|
| 812 |
+
""", unsafe_allow_html=True)
|
app/streamlit_app.py
CHANGED
|
@@ -23,7 +23,7 @@ from app.utils.video_downloader import download_youtube_video, download_pro_refe
|
|
| 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 |
|
|
@@ -451,29 +451,13 @@ def main():
|
|
| 451 |
elif llm_services['openai']['available']:
|
| 452 |
st.info("🤖 **Analysis generated using OpenAI**")
|
| 453 |
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
with drill1:
|
| 462 |
-
st.markdown("**Posture Drill**")
|
| 463 |
-
st.markdown("- Stand with your back against a wall")
|
| 464 |
-
st.markdown(
|
| 465 |
-
"- Take your golf stance while maintaining contact"
|
| 466 |
-
)
|
| 467 |
-
st.markdown(
|
| 468 |
-
"- Practice maintaining this posture during your swing"
|
| 469 |
-
)
|
| 470 |
-
|
| 471 |
-
with drill2:
|
| 472 |
-
st.markdown("**Tempo Drill**")
|
| 473 |
-
st.markdown("- Count '1-2-3' for your backswing")
|
| 474 |
-
st.markdown("- Count '1' for your downswing")
|
| 475 |
-
st.markdown("- Practice maintaining a 3:1 tempo ratio")
|
| 476 |
-
|
| 477 |
# Handle key frame analysis (new tab/option)
|
| 478 |
if keyframe_analysis_clicked:
|
| 479 |
try:
|
|
|
|
| 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 |
|
|
|
|
| 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:
|