Spaces:
Paused
Paused
Update Par-ity Project with enhanced features
Browse files- .gitattributes +1 -0
- README.md +179 -91
- app/golf_swing_rag.py +271 -0
- app/models/llm_analyzer.py +270 -323
- app/streamlit_app.py +470 -6
- app/utils/visualizer.py +4 -6
- article_extractor.py +83 -0
- golf_swing_articles_complete.csv +0 -0
- requirements.txt +13 -4
.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -8,113 +8,201 @@ app_file: app/streamlit_app.py
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
|
|
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
A tool for analyzing golf swings using computer vision and AI.
|
| 15 |
|
| 16 |
## Features
|
| 17 |
|
|
|
|
| 18 |
- Upload or provide YouTube links to golf swing videos
|
| 19 |
- Automated swing analysis using computer vision
|
| 20 |
- Pose estimation and tracking
|
| 21 |
-
- Swing phase segmentation
|
| 22 |
- Club and ball trajectory analysis
|
| 23 |
-
-
|
| 24 |
-
-
|
| 25 |
-
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
./run_streamlit.sh
|
| 75 |
```
|
| 76 |
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
```
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
```
|
| 81 |
|
| 82 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
-
|
|
|
|
|
|
|
|
|
|
| 85 |
2. Click "Analyze Swing" to process the video
|
| 86 |
-
3.
|
| 87 |
-
4.
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
|
| 95 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
|
| 97 |
-
|
| 98 |
-
- YOLOv8 for object detection
|
| 99 |
-
- MediaPipe for pose estimation
|
| 100 |
-
- OpenCV for video processing
|
| 101 |
-
- OpenAI GPT-4/3.5 or Ollama for swing analysis
|
| 102 |
-
- Streamlit for the web interface
|
| 103 |
-
|
| 104 |
-
## Directory Structure
|
| 105 |
-
|
| 106 |
-
- `app/`: Main application code
|
| 107 |
-
- `models/`: Analysis models
|
| 108 |
-
- `utils/`: Utility functions
|
| 109 |
-
- `components/`: UI components
|
| 110 |
-
- `streamlit_app.py`: Main Streamlit application
|
| 111 |
-
- `downloads/`: Downloaded and processed videos
|
| 112 |
-
- `requirements.txt`: Required Python packages
|
| 113 |
-
- `setup_directories.sh`: Script to set up required directories
|
| 114 |
-
- `run_streamlit.sh`: Script to run the Streamlit app
|
| 115 |
-
|
| 116 |
-
## Notes
|
| 117 |
-
|
| 118 |
-
- For best results, use videos where the golfer is clearly visible
|
| 119 |
-
- Side view videos work best for analysis
|
| 120 |
-
- Processing time depends on video length and resolution
|
|
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# Par-ity Project: Golf Swing Analysis with AI Assistant ⛳🏌️♀️
|
| 12 |
|
| 13 |
+
A comprehensive golf swing analysis platform that combines computer vision-based swing analysis with an AI-powered technique assistant. This integrated system provides both automated video analysis and expert knowledge retrieval for improving your golf swing.
|
|
|
|
|
|
|
| 14 |
|
| 15 |
## Features
|
| 16 |
|
| 17 |
+
### 🎥 Video Analysis
|
| 18 |
- Upload or provide YouTube links to golf swing videos
|
| 19 |
- Automated swing analysis using computer vision
|
| 20 |
- Pose estimation and tracking
|
| 21 |
+
- Swing phase segmentation (setup, backswing, downswing, follow-through)
|
| 22 |
- Club and ball trajectory analysis
|
| 23 |
+
- Annotated video generation with visual feedback
|
| 24 |
+
- Key position comparison (setup, top of backswing, impact)
|
| 25 |
+
- AI-powered improvement recommendations
|
| 26 |
+
|
| 27 |
+
### 🤖 Golf Swing Technique Assistant (RAG)
|
| 28 |
+
- **Expert Knowledge Base**: 2,000+ professional golf instruction articles
|
| 29 |
+
- **Semantic Search**: Ask questions in natural language
|
| 30 |
+
- **Contextual Answers**: Get detailed responses with source citations
|
| 31 |
+
- **Interactive Chat**: Build conversations about your swing technique
|
| 32 |
+
- **TPI Content**: Based on Titleist Performance Institute materials
|
| 33 |
+
|
| 34 |
+
## What You Can Do
|
| 35 |
+
|
| 36 |
+
### Video Analysis Options
|
| 37 |
+
After uploading a video, you get 4 analysis options:
|
| 38 |
+
|
| 39 |
+
1. **Generate Annotated Video** - Visual feedback showing swing phases and metrics
|
| 40 |
+
2. **Generate Improvement Recommendations** - AI-powered personalized tips
|
| 41 |
+
3. **Key Frame Analysis** - Detailed review of critical swing positions
|
| 42 |
+
4. **Golf Swing Chatbot** - Ask specific technique questions
|
| 43 |
+
|
| 44 |
+
### Example Questions for the AI Assistant
|
| 45 |
+
- "What wrist motion happens during the downswing?"
|
| 46 |
+
- "I'm having trouble with my slice, can you help?"
|
| 47 |
+
- "What should I focus on to increase my driving distance?"
|
| 48 |
+
- "How do I fix my inconsistent ball striking?"
|
| 49 |
+
- "What physical limitations can affect my swing?"
|
| 50 |
+
|
| 51 |
+
## Setup Instructions
|
| 52 |
+
|
| 53 |
+
### 1. Install Dependencies
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
pip install -r requirements.txt
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
### 2. Directory Setup
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
./setup_directories.sh
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
### 3. OpenAI API Key (Optional)
|
| 66 |
+
|
| 67 |
+
For enhanced AI responses, set up an OpenAI API key:
|
| 68 |
+
|
| 69 |
+
**Option 1: Environment File**
|
| 70 |
+
```bash
|
| 71 |
+
cp .env.example .env
|
| 72 |
+
# Edit .env and add your API key
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
**Option 2: Streamlit Secrets**
|
| 76 |
+
Create `.streamlit/secrets.toml`:
|
| 77 |
+
```toml
|
| 78 |
+
[openai]
|
| 79 |
+
api_key = "your-openai-api-key-here"
|
| 80 |
```
|
| 81 |
+
|
| 82 |
+
**Option 3: Enter in App**
|
| 83 |
+
You can also enter the API key directly in the Streamlit interface.
|
| 84 |
+
|
| 85 |
+
### 4. Run the Application
|
| 86 |
+
|
| 87 |
+
```bash
|
| 88 |
+
cd app
|
| 89 |
+
streamlit run streamlit_app.py
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
Or use the convenience script:
|
| 93 |
+
```bash
|
| 94 |
./run_streamlit.sh
|
| 95 |
```
|
| 96 |
|
| 97 |
+
## How It Works
|
| 98 |
+
|
| 99 |
+
### Video Analysis Pipeline
|
| 100 |
+
1. **Video Processing**: Extracts frames and detects objects using YOLOv8
|
| 101 |
+
2. **Pose Analysis**: Uses MediaPipe for detailed body positioning
|
| 102 |
+
3. **Swing Segmentation**: Identifies swing phases automatically
|
| 103 |
+
4. **Trajectory Analysis**: Tracks club and ball movement
|
| 104 |
+
5. **AI Recommendations**: Generates personalized improvement tips
|
| 105 |
+
|
| 106 |
+
### RAG (Retrieval-Augmented Generation) System
|
| 107 |
+
1. **Knowledge Processing**: Loads and processes 2,000+ golf instruction articles
|
| 108 |
+
2. **Semantic Embeddings**: Creates vector representations using Sentence Transformers
|
| 109 |
+
3. **Smart Search**: Uses FAISS for fast similarity search
|
| 110 |
+
4. **Response Generation**: Combines retrieved knowledge with AI (GPT-3.5) or fallback mode
|
| 111 |
+
|
| 112 |
+
## File Structure
|
| 113 |
+
|
| 114 |
```
|
| 115 |
+
Golf_Swing_Analysis/
|
| 116 |
+
├── app/ # Main application
|
| 117 |
+
│ ├── streamlit_app.py # Integrated Streamlit app
|
| 118 |
+
│ ├── golf_swing_rag.py # RAG system
|
| 119 |
+
│ ├── models/ # Analysis models
|
| 120 |
+
│ ├── utils/ # Utility functions
|
| 121 |
+
│ └── components/ # UI components
|
| 122 |
+
├── golf_swing_articles_complete.csv # Knowledge base (2,000+ articles)
|
| 123 |
+
├── requirements.txt # Python dependencies
|
| 124 |
+
├── .env.example # Environment variables template
|
| 125 |
+
├── test_rag_integration.py # Integration test script
|
| 126 |
+
└── Generated files (after first run):
|
| 127 |
+
├── golf_swing_embeddings.pkl # Cached embeddings
|
| 128 |
+
├── golf_swing_index.faiss # Vector search index
|
| 129 |
+
└── downloads/ # Processed videos
|
| 130 |
```
|
| 131 |
|
| 132 |
+
## Technical Details
|
| 133 |
+
|
| 134 |
+
### Technologies Used
|
| 135 |
+
- **Computer Vision**: YOLOv8, MediaPipe, OpenCV
|
| 136 |
+
- **AI/ML**: OpenAI GPT-3.5/4, Ollama (local LLM option)
|
| 137 |
+
- **RAG Stack**: Sentence Transformers, FAISS, LangChain
|
| 138 |
+
- **Web Interface**: Streamlit
|
| 139 |
+
- **Data Processing**: Pandas, NumPy
|
| 140 |
+
|
| 141 |
+
### Performance Features
|
| 142 |
+
- **Cached Embeddings**: First-time setup creates embeddings saved for future use
|
| 143 |
+
- **Efficient Search**: FAISS enables fast similarity search over thousands of chunks
|
| 144 |
+
- **Automatic Cleanup**: Temporary files are managed automatically
|
| 145 |
+
- **Batch Processing**: Video frames and embeddings processed efficiently
|
| 146 |
|
| 147 |
+
## Usage Guide
|
| 148 |
+
|
| 149 |
+
### 1. Video Analysis Workflow
|
| 150 |
+
1. Choose input method (YouTube URL or file upload)
|
| 151 |
2. Click "Analyze Swing" to process the video
|
| 152 |
+
3. Select from 4 analysis options
|
| 153 |
+
4. Download results and annotated videos
|
| 154 |
+
|
| 155 |
+
### 2. AI Assistant Workflow
|
| 156 |
+
1. Click "Golf Swing Chatbot" after video analysis (or use standalone)
|
| 157 |
+
2. Ask questions about golf swing technique
|
| 158 |
+
3. Review detailed answers with source citations
|
| 159 |
+
4. Build conversations for comprehensive understanding
|
| 160 |
+
|
| 161 |
+
## Example Use Cases
|
| 162 |
+
|
| 163 |
+
### Video Analysis
|
| 164 |
+
- **Beginner Golfer**: Upload practice swing video → Get annotated feedback → Learn proper positions
|
| 165 |
+
- **Intermediate Player**: Analyze driver swing → Get AI recommendations → Focus on specific improvements
|
| 166 |
+
- **Coach**: Use key frame analysis → Show students critical positions → Provide visual evidence
|
| 167 |
+
|
| 168 |
+
### AI Assistant
|
| 169 |
+
- **Technique Questions**: "How should my weight shift during the swing?"
|
| 170 |
+
- **Problem Solving**: "I keep hitting fat shots with my irons, what's wrong?"
|
| 171 |
+
- **Learning**: "Explain the biomechanics of the golf swing"
|
| 172 |
+
- **Specific Issues**: "I have limited hip mobility, how does this affect my swing?"
|
| 173 |
+
|
| 174 |
+
## Troubleshooting
|
| 175 |
+
|
| 176 |
+
### First Run Setup
|
| 177 |
+
- Initial embedding creation takes 5-10 minutes (one-time process)
|
| 178 |
+
- Ensure adequate RAM (8GB+ recommended) for large knowledge base
|
| 179 |
+
- Video processing time depends on length and resolution
|
| 180 |
+
|
| 181 |
+
### Common Issues
|
| 182 |
+
- **Missing Dependencies**: Run `pip install --upgrade -r requirements.txt`
|
| 183 |
+
- **Import Errors**: Ensure you're running from the correct directory
|
| 184 |
+
- **RAG Not Available**: Check that `golf_swing_articles_complete.csv` exists
|
| 185 |
+
- **Video Issues**: Ensure videos are in supported formats (MP4, MOV, AVI)
|
| 186 |
+
|
| 187 |
+
### Testing Integration
|
| 188 |
+
Run the test script to verify everything works:
|
| 189 |
+
```bash
|
| 190 |
+
python3 test_rag_integration.py
|
| 191 |
+
```
|
| 192 |
|
| 193 |
+
## Contributing
|
| 194 |
+
|
| 195 |
+
This system is designed to be extensible:
|
| 196 |
+
|
| 197 |
+
1. **Video Analysis**: Add new computer vision models or metrics
|
| 198 |
+
2. **Knowledge Base**: Include additional golf instruction sources
|
| 199 |
+
3. **AI Models**: Experiment with different embedding models or LLMs
|
| 200 |
+
4. **UI/UX**: Enhance the Streamlit interface with new features
|
| 201 |
+
|
| 202 |
+
## License
|
| 203 |
+
|
| 204 |
+
This project is for educational and personal use. The golf instruction content is sourced from publicly available articles and should be attributed to original sources.
|
| 205 |
+
|
| 206 |
+
---
|
| 207 |
|
| 208 |
+
**Built with ❤️ to empower golfers with AI-powered analysis and expert knowledge**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/golf_swing_rag.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
import faiss
|
| 4 |
+
from sentence_transformers import SentenceTransformer
|
| 5 |
+
import streamlit as st
|
| 6 |
+
import openai
|
| 7 |
+
from dotenv import load_dotenv
|
| 8 |
+
import os
|
| 9 |
+
import json
|
| 10 |
+
import pickle
|
| 11 |
+
from typing import List, Dict, Tuple
|
| 12 |
+
import re
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
|
| 15 |
+
# Load environment variables
|
| 16 |
+
load_dotenv()
|
| 17 |
+
|
| 18 |
+
class GolfSwingRAG:
|
| 19 |
+
def __init__(self, csv_file_path: str = None):
|
| 20 |
+
"""Initialize the Golf Swing RAG system"""
|
| 21 |
+
# Set default CSV path based on current working directory
|
| 22 |
+
if csv_file_path is None:
|
| 23 |
+
if os.path.exists("golf_swing_articles_complete.csv"):
|
| 24 |
+
csv_file_path = "golf_swing_articles_complete.csv"
|
| 25 |
+
elif os.path.exists("../golf_swing_articles_complete.csv"):
|
| 26 |
+
csv_file_path = "../golf_swing_articles_complete.csv"
|
| 27 |
+
else:
|
| 28 |
+
raise FileNotFoundError("golf_swing_articles_complete.csv not found in current or parent directory")
|
| 29 |
+
|
| 30 |
+
self.csv_file_path = csv_file_path
|
| 31 |
+
self.embedding_model = SentenceTransformer('all-MiniLM-L6-v2')
|
| 32 |
+
self.index = None
|
| 33 |
+
self.chunks = []
|
| 34 |
+
self.metadata = []
|
| 35 |
+
self.openai_client = None
|
| 36 |
+
|
| 37 |
+
# Initialize OpenAI client using Streamlit secrets
|
| 38 |
+
try:
|
| 39 |
+
openai_key = st.secrets.get("openai", {}).get("api_key", "")
|
| 40 |
+
if openai_key:
|
| 41 |
+
self.openai_client = openai.OpenAI(api_key=openai_key)
|
| 42 |
+
except (KeyError, FileNotFoundError, AttributeError):
|
| 43 |
+
# Fallback to environment variable if secrets not available
|
| 44 |
+
if os.getenv("OPENAI_API_KEY"):
|
| 45 |
+
self.openai_client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
| 46 |
+
|
| 47 |
+
def load_and_process_data(self):
|
| 48 |
+
"""Load CSV data and process it for RAG"""
|
| 49 |
+
print("Loading golf swing data...")
|
| 50 |
+
|
| 51 |
+
# Read CSV file
|
| 52 |
+
df = pd.read_csv(self.csv_file_path)
|
| 53 |
+
print(f"Loaded {len(df)} articles")
|
| 54 |
+
|
| 55 |
+
# Process each article
|
| 56 |
+
all_chunks = []
|
| 57 |
+
all_metadata = []
|
| 58 |
+
|
| 59 |
+
for idx, row in df.iterrows():
|
| 60 |
+
# Parse text chunks if they exist
|
| 61 |
+
text_chunks = []
|
| 62 |
+
if pd.notna(row['text_chunks']) and row['text_chunks'].strip():
|
| 63 |
+
try:
|
| 64 |
+
# Parse the text_chunks column (it appears to be a list in string format)
|
| 65 |
+
chunks_str = row['text_chunks']
|
| 66 |
+
if chunks_str.startswith('[') and chunks_str.endswith(']'):
|
| 67 |
+
# Remove brackets and split by quotes
|
| 68 |
+
chunks_str = chunks_str[1:-1] # Remove outer brackets
|
| 69 |
+
# Split by quote patterns while preserving content
|
| 70 |
+
text_chunks = [chunk.strip().strip("'\"") for chunk in chunks_str.split("', '") if chunk.strip()]
|
| 71 |
+
if not text_chunks and chunks_str:
|
| 72 |
+
text_chunks = [chunks_str.strip().strip("'\"")]
|
| 73 |
+
except:
|
| 74 |
+
# Fallback: use cleaned_text if text_chunks parsing fails
|
| 75 |
+
text_chunks = [row['cleaned_text']] if pd.notna(row['cleaned_text']) else []
|
| 76 |
+
|
| 77 |
+
# If no chunks, create chunks from cleaned_text or text
|
| 78 |
+
if not text_chunks:
|
| 79 |
+
text_content = row['cleaned_text'] if pd.notna(row['cleaned_text']) else row['text']
|
| 80 |
+
if pd.notna(text_content):
|
| 81 |
+
# Split into chunks of ~500 words
|
| 82 |
+
words = text_content.split()
|
| 83 |
+
chunk_size = 500
|
| 84 |
+
text_chunks = [' '.join(words[i:i+chunk_size]) for i in range(0, len(words), chunk_size)]
|
| 85 |
+
|
| 86 |
+
# Add each chunk with metadata
|
| 87 |
+
for chunk_idx, chunk in enumerate(text_chunks):
|
| 88 |
+
if chunk and len(chunk.strip()) > 50: # Only process substantial chunks
|
| 89 |
+
all_chunks.append(chunk)
|
| 90 |
+
all_metadata.append({
|
| 91 |
+
'title': row['title'],
|
| 92 |
+
'url': row['url'],
|
| 93 |
+
'source': row['source'],
|
| 94 |
+
'publish_date': row['publish_date'],
|
| 95 |
+
'authors': row['authors'],
|
| 96 |
+
'chunk_index': chunk_idx,
|
| 97 |
+
'article_index': idx
|
| 98 |
+
})
|
| 99 |
+
|
| 100 |
+
self.chunks = all_chunks
|
| 101 |
+
self.metadata = all_metadata
|
| 102 |
+
print(f"Created {len(all_chunks)} text chunks")
|
| 103 |
+
|
| 104 |
+
def create_embeddings(self, force_recreate: bool = False):
|
| 105 |
+
"""Create embeddings for all text chunks"""
|
| 106 |
+
# Determine the correct base directory for embeddings files
|
| 107 |
+
if os.path.exists("golf_swing_articles_complete.csv"):
|
| 108 |
+
# Running from project root
|
| 109 |
+
embeddings_file = "golf_swing_embeddings.pkl"
|
| 110 |
+
index_file = "golf_swing_index.faiss"
|
| 111 |
+
else:
|
| 112 |
+
# Running from app directory
|
| 113 |
+
embeddings_file = "../golf_swing_embeddings.pkl"
|
| 114 |
+
index_file = "../golf_swing_index.faiss"
|
| 115 |
+
|
| 116 |
+
if not force_recreate and os.path.exists(embeddings_file) and os.path.exists(index_file):
|
| 117 |
+
print("Loading existing embeddings...")
|
| 118 |
+
with open(embeddings_file, 'rb') as f:
|
| 119 |
+
data = pickle.load(f)
|
| 120 |
+
self.chunks = data['chunks']
|
| 121 |
+
self.metadata = data['metadata']
|
| 122 |
+
self.index = faiss.read_index(index_file)
|
| 123 |
+
print(f"Loaded {len(self.chunks)} chunks with embeddings")
|
| 124 |
+
return
|
| 125 |
+
|
| 126 |
+
print("Creating embeddings...")
|
| 127 |
+
if not self.chunks:
|
| 128 |
+
self.load_and_process_data()
|
| 129 |
+
|
| 130 |
+
# Create embeddings in batches
|
| 131 |
+
batch_size = 32
|
| 132 |
+
all_embeddings = []
|
| 133 |
+
|
| 134 |
+
for i in range(0, len(self.chunks), batch_size):
|
| 135 |
+
batch_chunks = self.chunks[i:i+batch_size]
|
| 136 |
+
batch_embeddings = self.embedding_model.encode(batch_chunks, show_progress_bar=True)
|
| 137 |
+
all_embeddings.append(batch_embeddings)
|
| 138 |
+
print(f"Processed {min(i+batch_size, len(self.chunks))}/{len(self.chunks)} chunks")
|
| 139 |
+
|
| 140 |
+
# Combine all embeddings
|
| 141 |
+
embeddings = np.vstack(all_embeddings)
|
| 142 |
+
|
| 143 |
+
# Create FAISS index
|
| 144 |
+
dimension = embeddings.shape[1]
|
| 145 |
+
self.index = faiss.IndexFlatIP(dimension) # Inner product for cosine similarity
|
| 146 |
+
|
| 147 |
+
# Normalize embeddings for cosine similarity
|
| 148 |
+
faiss.normalize_L2(embeddings)
|
| 149 |
+
self.index.add(embeddings)
|
| 150 |
+
|
| 151 |
+
# Save embeddings and index
|
| 152 |
+
with open(embeddings_file, 'wb') as f:
|
| 153 |
+
pickle.dump({
|
| 154 |
+
'chunks': self.chunks,
|
| 155 |
+
'metadata': self.metadata
|
| 156 |
+
}, f)
|
| 157 |
+
faiss.write_index(self.index, index_file)
|
| 158 |
+
|
| 159 |
+
print(f"Created and saved embeddings for {len(self.chunks)} chunks")
|
| 160 |
+
|
| 161 |
+
def search_similar_chunks(self, query: str, top_k: int = 5) -> List[Dict]:
|
| 162 |
+
"""Search for similar chunks using semantic similarity"""
|
| 163 |
+
# Create query embedding
|
| 164 |
+
query_embedding = self.embedding_model.encode([query])
|
| 165 |
+
faiss.normalize_L2(query_embedding)
|
| 166 |
+
|
| 167 |
+
# Search in FAISS index
|
| 168 |
+
scores, indices = self.index.search(query_embedding, top_k)
|
| 169 |
+
|
| 170 |
+
results = []
|
| 171 |
+
for score, idx in zip(scores[0], indices[0]):
|
| 172 |
+
if idx < len(self.chunks): # Valid index
|
| 173 |
+
results.append({
|
| 174 |
+
'chunk': self.chunks[idx],
|
| 175 |
+
'metadata': self.metadata[idx],
|
| 176 |
+
'similarity_score': float(score)
|
| 177 |
+
})
|
| 178 |
+
|
| 179 |
+
return results
|
| 180 |
+
|
| 181 |
+
def generate_response(self, query: str, context_chunks: List[Dict]) -> str:
|
| 182 |
+
"""Generate response using OpenAI API with context"""
|
| 183 |
+
if not self.openai_client:
|
| 184 |
+
return self._generate_fallback_response(query, context_chunks)
|
| 185 |
+
|
| 186 |
+
# Prepare context
|
| 187 |
+
context = "\n\n".join([f"Source: {chunk['metadata']['title']}\nContent: {chunk['chunk']}"
|
| 188 |
+
for chunk in context_chunks])
|
| 189 |
+
|
| 190 |
+
# Create system prompt
|
| 191 |
+
system_prompt = """You are a golf swing technique expert assistant. You help golfers improve their swing by providing detailed, accurate advice based on professional golf instruction content.
|
| 192 |
+
|
| 193 |
+
Instructions:
|
| 194 |
+
- Answer questions about golf swing technique, mechanics, common problems, and solutions
|
| 195 |
+
- Provide specific, actionable advice when possible
|
| 196 |
+
- Reference relevant technical concepts when appropriate
|
| 197 |
+
- Be encouraging and supportive
|
| 198 |
+
- If asked about physical limitations or injuries, recommend consulting with a TPI certified professional
|
| 199 |
+
- Always base your answers on the provided context from golf instruction materials
|
| 200 |
+
|
| 201 |
+
Context from golf instruction database:
|
| 202 |
+
{context}"""
|
| 203 |
+
|
| 204 |
+
user_prompt = f"""Based on the golf instruction content provided, please answer this question about golf swing technique:
|
| 205 |
+
|
| 206 |
+
Question: {query}
|
| 207 |
+
|
| 208 |
+
Please provide a helpful, detailed response that addresses the specific question while drawing from the relevant information in the context."""
|
| 209 |
+
|
| 210 |
+
try:
|
| 211 |
+
response = self.openai_client.chat.completions.create(
|
| 212 |
+
model="gpt-3.5-turbo",
|
| 213 |
+
messages=[
|
| 214 |
+
{"role": "system", "content": system_prompt.format(context=context)},
|
| 215 |
+
{"role": "user", "content": user_prompt}
|
| 216 |
+
],
|
| 217 |
+
max_tokens=1000,
|
| 218 |
+
temperature=0.7
|
| 219 |
+
)
|
| 220 |
+
return response.choices[0].message.content
|
| 221 |
+
except Exception as e:
|
| 222 |
+
print(f"OpenAI API error: {e}")
|
| 223 |
+
return self._generate_fallback_response(query, context_chunks)
|
| 224 |
+
|
| 225 |
+
def _generate_fallback_response(self, query: str, context_chunks: List[Dict]) -> str:
|
| 226 |
+
"""Generate a fallback response when OpenAI API is not available"""
|
| 227 |
+
if not context_chunks:
|
| 228 |
+
return "I couldn't find specific information about that topic in the golf swing database. Could you try rephrasing your question or being more specific?"
|
| 229 |
+
|
| 230 |
+
# Create a simple response based on the most relevant chunk
|
| 231 |
+
best_chunk = context_chunks[0]
|
| 232 |
+
chunk_content = best_chunk['chunk']
|
| 233 |
+
title = best_chunk['metadata']['title']
|
| 234 |
+
|
| 235 |
+
response = f"Based on the article '{title}', here's what I found:\n\n"
|
| 236 |
+
response += chunk_content[:500] + "..."
|
| 237 |
+
response += f"\n\nFor more detailed information, you can refer to the full article: {title}"
|
| 238 |
+
|
| 239 |
+
return response
|
| 240 |
+
|
| 241 |
+
def query(self, question: str, top_k: int = 5) -> Dict:
|
| 242 |
+
"""Main query method that returns both response and sources"""
|
| 243 |
+
# Search for relevant chunks
|
| 244 |
+
relevant_chunks = self.search_similar_chunks(question, top_k)
|
| 245 |
+
|
| 246 |
+
# Generate response
|
| 247 |
+
response = self.generate_response(question, relevant_chunks)
|
| 248 |
+
|
| 249 |
+
return {
|
| 250 |
+
'response': response,
|
| 251 |
+
'sources': relevant_chunks,
|
| 252 |
+
'query': question,
|
| 253 |
+
'timestamp': datetime.now().isoformat()
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
def main():
|
| 257 |
+
"""Initialize and test the RAG system"""
|
| 258 |
+
rag = GolfSwingRAG()
|
| 259 |
+
rag.load_and_process_data()
|
| 260 |
+
rag.create_embeddings()
|
| 261 |
+
|
| 262 |
+
# Test query
|
| 263 |
+
test_query = "What wrist motion happens during the downswing?"
|
| 264 |
+
result = rag.query(test_query)
|
| 265 |
+
|
| 266 |
+
print(f"Query: {result['query']}")
|
| 267 |
+
print(f"Response: {result['response']}")
|
| 268 |
+
print(f"Number of sources: {len(result['sources'])}")
|
| 269 |
+
|
| 270 |
+
if __name__ == "__main__":
|
| 271 |
+
main()
|
app/models/llm_analyzer.py
CHANGED
|
@@ -392,29 +392,43 @@ Use these professional standards as your 100% reference for scoring. These repre
|
|
| 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**:
|
| 398 |
-
- **Shoulder Rotation**: 120° (
|
| 399 |
-
- **Posture Score**: 95-
|
| 400 |
-
- **Weight Shift**:
|
| 401 |
|
| 402 |
**Upper Body Excellence:**
|
| 403 |
-
- **Arm Extension**:
|
| 404 |
-
- **Wrist Hinge**:
|
| 405 |
-
- **Swing Plane Consistency**: 85% (
|
| 406 |
-
- **Chest Rotation Efficiency**: 100% (
|
| 407 |
|
| 408 |
**Power & Efficiency Markers:**
|
| 409 |
-
- **Energy Transfer Efficiency**:
|
| 410 |
-
- **Power Accumulation**: 100% (
|
| 411 |
-
- **Sequential Kinematic Sequence**: 100% (
|
| 412 |
-
- **Potential Distance**:
|
| 413 |
|
| 414 |
**Movement Quality Standards:**
|
| 415 |
-
- **Head Movement**:
|
| 416 |
-
- **Ground Force Efficiency**:
|
| 417 |
-
- **Hip Thrust**:
|
| 418 |
|
| 419 |
### **AMATEUR REFERENCE EXAMPLES FOR CALIBRATION:**
|
| 420 |
|
|
@@ -427,24 +441,6 @@ Use these professional standards as your 100% reference for scoring. These repre
|
|
| 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)
|
|
@@ -455,14 +451,19 @@ Use these professional standards as your 100% reference for scoring. These repre
|
|
| 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
|
| 460 |
-
2. **Shoulder Rotation
|
| 461 |
-
3. **
|
| 462 |
-
4. **
|
| 463 |
-
5. **
|
| 464 |
-
6. **Energy Transfer
|
| 465 |
-
7. **
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
|
| 467 |
## CURRENT SWING ANALYSIS
|
| 468 |
|
|
@@ -523,125 +524,72 @@ Use these professional standards as your 100% reference for scoring. These repre
|
|
| 523 |
|
| 524 |
## ANALYSIS INSTRUCTIONS
|
| 525 |
|
| 526 |
-
|
|
|
|
| 527 |
|
| 528 |
-
**PERFORMANCE_CLASSIFICATION:** [XX%]
|
|
|
|
| 529 |
|
| 530 |
**STRENGTHS:**
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
|
|
|
|
|
|
| 534 |
|
| 535 |
-
|
| 536 |
-
•
|
| 537 |
-
• [Another area with impact description - e.g. "Your head movement during the swing is more than ideal, which could be affecting your accuracy and consistency"]
|
| 538 |
-
• [Third area with impact-focused description - e.g. "Your wrist action could use some work, which may be affecting your ability to generate lag and create powerful impact"]
|
| 539 |
|
| 540 |
-
**
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
**For STRENGTHS** - Must include:
|
| 548 |
-
- EXACTLY 3 bullet points - no more, no less
|
| 549 |
-
- Use encouraging, positive language
|
| 550 |
-
- NO numbers or statistics - focus on qualitative descriptions
|
| 551 |
-
- Reference professional standards without mentioning specific metrics
|
| 552 |
-
- Recognition when mechanics are working well
|
| 553 |
-
- Explain timing in swing when relevant (during backswing, at impact, etc.)
|
| 554 |
-
- Use supportive tone appropriate for young golfers
|
| 555 |
-
|
| 556 |
-
**For WEAKNESSES** - Must include:
|
| 557 |
-
- EXACTLY 3 bullet points - no more, no less
|
| 558 |
-
- NO numbers, degrees, or specific measurements
|
| 559 |
-
- Focus on the IMPACT of the issue (what it's affecting) rather than the measurement
|
| 560 |
-
- Use phrases like "less than optimal" or "more than ideal" instead of specific amounts
|
| 561 |
-
- Explain HOW the weakness affects performance (power, accuracy, consistency, etc.)
|
| 562 |
-
- DO NOT provide improvement suggestions - save those for the Priority Improvements section
|
| 563 |
-
- Frame as areas that may be affecting performance rather than deficiencies
|
| 564 |
-
|
| 565 |
-
**For PRIORITY_IMPROVEMENTS** - Must include:
|
| 566 |
-
- EXACTLY 3 numbered items - no more, no less
|
| 567 |
-
- NO header formatting like [Most Critical], [Important], [Focus Area] in the descriptions
|
| 568 |
-
- Use encouraging language like "try increasing" or "focus on" instead of "you need to"
|
| 569 |
-
- When in the swing this should happen (during downswing, backswing, at impact, etc.)
|
| 570 |
-
- Reference professional standards gently without excessive numerical comparisons
|
| 571 |
-
- Clear explanation of benefits and positive outcomes
|
| 572 |
-
- Maintain supportive, coaching tone throughout
|
| 573 |
-
|
| 574 |
-
**EXAMPLE ANALYSIS STRUCTURE:**
|
| 575 |
-
|
| 576 |
-
**STRENGTHS:**
|
| 577 |
-
• Your shoulder rotation shows great upper body mobility during your backswing, matching what we see in professional swings
|
| 578 |
-
• Your weight transfer demonstrates excellent fundamentals in shifting from your back foot to your front foot through impact
|
| 579 |
-
• Your posture maintains good stability throughout most of your swing, showing solid foundational mechanics
|
| 580 |
|
| 581 |
-
|
| 582 |
-
• Your hip rotation is less than optimal, which may
|
| 583 |
-
• Your head movement during the swing is more than ideal, which could be affecting your accuracy and consistency throughout your shots
|
| 584 |
-
• Your wrist action could use some work, which may be affecting your ability to generate lag and create powerful impact
|
| 585 |
|
| 586 |
**PRIORITY_IMPROVEMENTS:**
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
-
|
| 625 |
-
-
|
| 626 |
-
- **Sequential Kinematic <80%**: Score below 70%, reference professional standards (100%)
|
| 627 |
-
- **Power Accumulation <90%**: Score below 80%, compare to professional benchmarks (100%)
|
| 628 |
-
- **Head Movement >10 inches**: Major limitation, compare to professional standards (2-8in)
|
| 629 |
-
- **Weight Shift <60%**: Significant weakness, reference professional range (70-88%)
|
| 630 |
-
|
| 631 |
-
IMPORTANT FORMATTING RULES:
|
| 632 |
-
- Use the exact headers shown above (PERFORMANCE_CLASSIFICATION, STRENGTHS, WEAKNESSES, PRIORITY_IMPROVEMENTS)
|
| 633 |
-
- For performance classification, use format: [XX%] where XX is the percentage (10-100)
|
| 634 |
-
- For strengths and weaknesses, use bullet points (•)
|
| 635 |
-
- For priority improvements, use numbered format (1., 2., 3.) with priority level in brackets
|
| 636 |
-
- Each priority improvement must have: [Priority Level] Topic Name - Full description with professional benchmark comparisons
|
| 637 |
-
- **MANDATORY**: Include specific metric values and professional benchmark comparisons in every strength, weakness, and improvement
|
| 638 |
-
- **MANDATORY**: Reference professional standards in analysis content
|
| 639 |
-
- Provide clear directional guidance (more/less rotation, when in swing) rather than overly technical numerical comparisons
|
| 640 |
-
- Focus analysis on biomechanical consistency rather than timing variations
|
| 641 |
-
- **CRITICAL**: Every analysis point must tie back to the professional benchmarks provided
|
| 642 |
-
- Avoid absolute language like "perfect" or "flawless" - use terms like "very good" or "meets standards"
|
| 643 |
-
|
| 644 |
-
Remember: Use the professional benchmarks (Atthaya Thitikul: 63.4° hip, 120° shoulder, 96.1% energy transfer, etc.) as the foundation for ALL analysis content, not just the percentage classification. Every strength, weakness, and improvement recommendation must include specific comparisons to professional standards with clear, actionable guidance on what needs to improve and when in the swing.
|
| 645 |
"""
|
| 646 |
|
| 647 |
return prompt
|
|
@@ -707,22 +655,63 @@ def parse_and_format_analysis(raw_analysis):
|
|
| 707 |
priority_match = re.search(r'\*\*PRIORITY_IMPROVEMENTS:\*\*\s*(.*?)$', raw_analysis, re.IGNORECASE | re.DOTALL)
|
| 708 |
if priority_match:
|
| 709 |
priority_text = priority_match.group(1)
|
| 710 |
-
#
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
description
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 726 |
|
| 727 |
# Fallback parsing if structured format wasn't used
|
| 728 |
if not formatted_analysis['strengths']:
|
|
@@ -794,27 +783,27 @@ def parse_and_format_analysis(raw_analysis):
|
|
| 794 |
percentage = formatted_analysis['classification']
|
| 795 |
if percentage >= 80:
|
| 796 |
formatted_analysis['priority_improvements'] = [
|
| 797 |
-
{'rank': 1, 'description': '
|
| 798 |
-
{'rank': 2, 'description': '
|
| 799 |
-
{'rank': 3, 'description': '
|
| 800 |
]
|
| 801 |
elif percentage >= 60:
|
| 802 |
formatted_analysis['priority_improvements'] = [
|
| 803 |
-
{'rank': 1, 'description': '
|
| 804 |
-
{'rank': 2, 'description': '
|
| 805 |
-
{'rank': 3, 'description': '
|
| 806 |
]
|
| 807 |
elif percentage >= 40:
|
| 808 |
formatted_analysis['priority_improvements'] = [
|
| 809 |
-
{'rank': 1, 'description': '
|
| 810 |
-
{'rank': 2, 'description': '
|
| 811 |
-
{'rank': 3, 'description': '
|
| 812 |
]
|
| 813 |
else: # Below 40%
|
| 814 |
formatted_analysis['priority_improvements'] = [
|
| 815 |
-
{'rank': 1, 'description': '
|
| 816 |
-
{'rank': 2, 'description': '
|
| 817 |
-
{'rank': 3, 'description': '
|
| 818 |
]
|
| 819 |
|
| 820 |
return formatted_analysis
|
|
@@ -951,68 +940,15 @@ def display_formatted_analysis(analysis_data):
|
|
| 951 |
rank = priority['rank']
|
| 952 |
description = priority['description']
|
| 953 |
|
| 954 |
-
#
|
| 955 |
-
|
| 956 |
-
desc = description
|
| 957 |
-
|
| 958 |
-
# Try different patterns to extract the main topic
|
| 959 |
-
if '[Most Critical]' in description or '[Important]' in description or '[Focus Area]' in description:
|
| 960 |
-
# Pattern: [Priority Level] Topic - Description
|
| 961 |
-
pattern = r'\[(.*?)\]\s*(.*?)(?:\s*-\s*(.*))?$'
|
| 962 |
-
match = re.search(pattern, description)
|
| 963 |
-
if match:
|
| 964 |
-
priority_level = match.group(1)
|
| 965 |
-
area = match.group(2).strip()
|
| 966 |
-
desc = match.group(3).strip() if match.group(3) else ""
|
| 967 |
-
elif ':' in description:
|
| 968 |
-
# Pattern: Topic: Description
|
| 969 |
parts = description.split(':', 1)
|
| 970 |
-
|
| 971 |
-
desc = parts[1].strip()
|
| 972 |
-
elif ' - ' in description:
|
| 973 |
-
# Pattern: Topic - Description
|
| 974 |
-
parts = description.split(' - ', 1)
|
| 975 |
-
area = parts[0].strip()
|
| 976 |
desc = parts[1].strip()
|
|
|
|
| 977 |
else:
|
| 978 |
-
#
|
| 979 |
-
|
| 980 |
-
if len(words) > 5:
|
| 981 |
-
# Take first 3-5 words as the area
|
| 982 |
-
area = ' '.join(words[:4])
|
| 983 |
-
desc = ' '.join(words[4:])
|
| 984 |
-
else:
|
| 985 |
-
area = description
|
| 986 |
-
desc = ""
|
| 987 |
-
|
| 988 |
-
# Clean up area and description
|
| 989 |
-
area = area.replace('[Most Critical]', '').replace('[Important]', '').replace('[Focus Area]', '').strip()
|
| 990 |
-
|
| 991 |
-
# Ensure we have meaningful content
|
| 992 |
-
if not area or len(area) < 5:
|
| 993 |
-
area = f"Priority {rank} Improvement"
|
| 994 |
-
|
| 995 |
-
if not desc or len(desc) < 10:
|
| 996 |
-
# Provide a more complete description based on the area
|
| 997 |
-
if 'posture' in area.lower():
|
| 998 |
-
desc = "Try working on maintaining proper spine angle and athletic stance throughout the swing for better consistency and power transfer."
|
| 999 |
-
elif 'tempo' in area.lower() or 'timing' in area.lower():
|
| 1000 |
-
desc = "Focus on developing a smooth, consistent rhythm that allows for proper sequencing of body movements."
|
| 1001 |
-
elif 'rotation' in area.lower():
|
| 1002 |
-
desc = "Try improving the coordination and range of motion in your body turn to generate more power and accuracy."
|
| 1003 |
-
elif 'weight' in area.lower() or 'shift' in area.lower():
|
| 1004 |
-
desc = "Practice transferring weight from back foot to front foot during the swing for better balance and power."
|
| 1005 |
-
elif 'knee' in area.lower():
|
| 1006 |
-
desc = "Work on maintaining proper knee flex and stability throughout the swing for better foundation and consistency."
|
| 1007 |
-
elif 'hip' in area.lower():
|
| 1008 |
-
desc = "Focus on improving hip mobility and rotation during the downswing to enhance power generation and sequencing."
|
| 1009 |
-
elif 'chest' in area.lower():
|
| 1010 |
-
desc = "Try improving chest rotation efficiency to better coordinate upper body movement with the swing sequence."
|
| 1011 |
-
else:
|
| 1012 |
-
desc = description # Use the full description if we can't categorize it
|
| 1013 |
-
|
| 1014 |
-
# Display using simple bullet points instead of colored boxes
|
| 1015 |
-
st.markdown(f"**{rank}. {area}:** {desc}")
|
| 1016 |
|
| 1017 |
st.write("") # Add spacing between items
|
| 1018 |
|
|
@@ -1028,7 +964,43 @@ def calculate_biomechanical_metrics(pose_data, swing_phases):
|
|
| 1028 |
Returns:
|
| 1029 |
dict: Calculated biomechanical metrics
|
| 1030 |
"""
|
| 1031 |
-
metrics
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1032 |
|
| 1033 |
# Get key frames for analysis
|
| 1034 |
setup_frames = swing_phases.get("setup", [])
|
|
@@ -1055,64 +1027,58 @@ def calculate_biomechanical_metrics(pose_data, swing_phases):
|
|
| 1055 |
setup_keypoints = pose_data[setup_frame]
|
| 1056 |
backswing_keypoints = pose_data[top_backswing_frame]
|
| 1057 |
|
| 1058 |
-
if len(setup_keypoints) >=
|
| 1059 |
# Hip rotation calculation using hip landmarks
|
| 1060 |
-
setup_left_hip = np.array(setup_keypoints
|
| 1061 |
-
setup_right_hip = np.array(setup_keypoints
|
| 1062 |
-
backswing_left_hip = np.array(backswing_keypoints
|
| 1063 |
-
backswing_right_hip = np.array(backswing_keypoints
|
| 1064 |
|
| 1065 |
# Calculate hip line angles
|
| 1066 |
setup_hip_vector = setup_right_hip - setup_left_hip
|
| 1067 |
backswing_hip_vector = backswing_right_hip - backswing_left_hip
|
| 1068 |
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
metrics["hip_rotation"] = 25 # Lower default for incomplete data
|
| 1077 |
-
else:
|
| 1078 |
-
metrics["hip_rotation"] = 25
|
| 1079 |
|
| 1080 |
# Calculate Shoulder Rotation
|
| 1081 |
if setup_frame and top_backswing_frame and setup_frame in pose_data and top_backswing_frame in pose_data:
|
| 1082 |
setup_keypoints = pose_data[setup_frame]
|
| 1083 |
backswing_keypoints = pose_data[top_backswing_frame]
|
| 1084 |
|
| 1085 |
-
if len(setup_keypoints) >=
|
| 1086 |
# Shoulder rotation calculation
|
| 1087 |
-
setup_left_shoulder = np.array(setup_keypoints
|
| 1088 |
-
setup_right_shoulder = np.array(setup_keypoints
|
| 1089 |
-
backswing_left_shoulder = np.array(backswing_keypoints
|
| 1090 |
-
backswing_right_shoulder = np.array(backswing_keypoints
|
| 1091 |
|
| 1092 |
setup_shoulder_vector = setup_right_shoulder - setup_left_shoulder
|
| 1093 |
backswing_shoulder_vector = backswing_right_shoulder - backswing_left_shoulder
|
| 1094 |
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
metrics["shoulder_rotation"] = 60 # Lower default
|
| 1102 |
-
else:
|
| 1103 |
-
metrics["shoulder_rotation"] = 60
|
| 1104 |
|
| 1105 |
# Calculate Weight Shift (using hip and ankle positions)
|
| 1106 |
if setup_frame and impact_frame and setup_frame in pose_data and impact_frame in pose_data:
|
| 1107 |
setup_keypoints = pose_data[setup_frame]
|
| 1108 |
impact_keypoints = pose_data[impact_frame]
|
| 1109 |
|
| 1110 |
-
if len(setup_keypoints) >=
|
| 1111 |
# Use center of mass approximation
|
| 1112 |
-
setup_left_ankle = np.array(setup_keypoints
|
| 1113 |
-
setup_right_ankle = np.array(setup_keypoints
|
| 1114 |
-
impact_left_ankle = np.array(impact_keypoints
|
| 1115 |
-
impact_right_ankle = np.array(impact_keypoints
|
| 1116 |
|
| 1117 |
# Calculate weight distribution based on foot positioning
|
| 1118 |
setup_center = (setup_left_ankle + setup_right_ankle) / 2
|
|
@@ -1124,47 +1090,40 @@ def calculate_biomechanical_metrics(pose_data, swing_phases):
|
|
| 1124 |
weight_shift_amount = np.linalg.norm(impact_center - setup_center) / foot_width
|
| 1125 |
# Convert to percentage (professionals typically achieve 70%+ to front foot)
|
| 1126 |
weight_shift = min(0.5 + weight_shift_amount * 0.5, 0.9)
|
| 1127 |
-
|
| 1128 |
-
weight_shift = 0.5
|
| 1129 |
-
metrics["weight_shift"] = weight_shift
|
| 1130 |
-
else:
|
| 1131 |
-
metrics["weight_shift"] = 0.5 # Neutral default
|
| 1132 |
-
else:
|
| 1133 |
-
metrics["weight_shift"] = 0.5
|
| 1134 |
|
| 1135 |
# Calculate Posture Score (spine angle consistency)
|
| 1136 |
posture_scores = []
|
| 1137 |
for frame_list in [setup_frames, backswing_frames, impact_frames]:
|
| 1138 |
if frame_list:
|
| 1139 |
frame = frame_list[len(frame_list) // 2]
|
| 1140 |
-
if frame in pose_data and len(pose_data[frame]) >=
|
| 1141 |
keypoints = pose_data[frame]
|
| 1142 |
# Use shoulder and hip landmarks to estimate spine angle
|
| 1143 |
-
left_shoulder = np.array(keypoints
|
| 1144 |
-
right_shoulder = np.array(keypoints
|
| 1145 |
-
left_hip = np.array(keypoints
|
| 1146 |
-
right_hip = np.array(keypoints
|
| 1147 |
|
| 1148 |
shoulder_center = (left_shoulder + right_shoulder) / 2
|
| 1149 |
hip_center = (left_hip + right_hip) / 2
|
| 1150 |
|
| 1151 |
spine_vector = shoulder_center - hip_center
|
| 1152 |
-
|
| 1153 |
-
|
|
|
|
| 1154 |
|
| 1155 |
if posture_scores:
|
| 1156 |
# Good posture = consistent spine angle across phases
|
| 1157 |
posture_consistency = 1.0 - (np.std(posture_scores) / 90.0) # Normalize by 90 degrees
|
| 1158 |
metrics["posture_score"] = max(0.3, min(posture_consistency, 1.0))
|
| 1159 |
-
else:
|
| 1160 |
-
metrics["posture_score"] = 0.6
|
| 1161 |
|
| 1162 |
# Calculate Arm Extension at Impact
|
| 1163 |
-
if impact_frame and impact_frame in pose_data and len(pose_data[impact_frame]) >=
|
| 1164 |
keypoints = pose_data[impact_frame]
|
| 1165 |
-
right_shoulder = np.array(keypoints
|
| 1166 |
-
right_elbow = np.array(keypoints
|
| 1167 |
-
right_wrist = np.array(keypoints
|
| 1168 |
|
| 1169 |
# Calculate arm extension
|
| 1170 |
upper_arm = np.linalg.norm(right_elbow - right_shoulder)
|
|
@@ -1177,10 +1136,6 @@ def calculate_biomechanical_metrics(pose_data, swing_phases):
|
|
| 1177 |
if total_arm_length > 0:
|
| 1178 |
extension_ratio = actual_distance / total_arm_length
|
| 1179 |
metrics["arm_extension"] = min(extension_ratio, 1.0)
|
| 1180 |
-
else:
|
| 1181 |
-
metrics["arm_extension"] = 0.6
|
| 1182 |
-
else:
|
| 1183 |
-
metrics["arm_extension"] = 0.6
|
| 1184 |
|
| 1185 |
# Calculate Wrist Hinge using joint angles
|
| 1186 |
wrist_angles = []
|
|
@@ -1188,32 +1143,34 @@ def calculate_biomechanical_metrics(pose_data, swing_phases):
|
|
| 1188 |
if frame_list:
|
| 1189 |
frame = frame_list[len(frame_list) // 2]
|
| 1190 |
if frame in pose_data:
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
|
|
|
|
|
|
|
|
|
| 1194 |
|
| 1195 |
if wrist_angles:
|
| 1196 |
avg_wrist_angle = np.mean(wrist_angles)
|
| 1197 |
# Good wrist hinge is typically 80+ degrees
|
| 1198 |
metrics["wrist_hinge"] = min(avg_wrist_angle, 120)
|
| 1199 |
-
else:
|
| 1200 |
-
metrics["wrist_hinge"] = 60
|
| 1201 |
|
| 1202 |
# Calculate Head Movement (lateral and vertical)
|
| 1203 |
if setup_frame and impact_frame and setup_frame in pose_data and impact_frame in pose_data:
|
| 1204 |
setup_keypoints = pose_data[setup_frame]
|
| 1205 |
impact_keypoints = pose_data[impact_frame]
|
| 1206 |
|
| 1207 |
-
if len(setup_keypoints) >=
|
| 1208 |
# Use nose landmark (index 0) for head position
|
| 1209 |
-
setup_head = np.array(setup_keypoints
|
| 1210 |
-
impact_head = np.array(impact_keypoints
|
| 1211 |
|
| 1212 |
head_movement = np.abs(impact_head - setup_head)
|
| 1213 |
# Convert pixel movement to approximate inches (rough estimation)
|
| 1214 |
# Assume average person's head is about 9 inches, use that as scale
|
| 1215 |
if len(setup_keypoints) > 10: # Have enough landmarks
|
| 1216 |
-
|
|
|
|
| 1217 |
if head_height_pixels > 0:
|
| 1218 |
pixel_to_inch = 4.0 / head_height_pixels # Approximate nose-to-mouth is 4 inches
|
| 1219 |
lateral_movement = head_movement[0] * pixel_to_inch
|
|
@@ -1227,24 +1184,18 @@ def calculate_biomechanical_metrics(pose_data, swing_phases):
|
|
| 1227 |
|
| 1228 |
metrics["head_movement_lateral"] = min(lateral_movement, 8.0)
|
| 1229 |
metrics["head_movement_vertical"] = min(vertical_movement, 6.0)
|
| 1230 |
-
else:
|
| 1231 |
-
metrics["head_movement_lateral"] = 3.0
|
| 1232 |
-
metrics["head_movement_vertical"] = 2.0
|
| 1233 |
-
else:
|
| 1234 |
-
metrics["head_movement_lateral"] = 3.0
|
| 1235 |
-
metrics["head_movement_vertical"] = 2.0
|
| 1236 |
|
| 1237 |
# Calculate Knee Flexion
|
| 1238 |
knee_flexions = {}
|
| 1239 |
for phase_name, frame_list in [("address", setup_frames), ("impact", impact_frames)]:
|
| 1240 |
if frame_list:
|
| 1241 |
frame = frame_list[len(frame_list) // 2]
|
| 1242 |
-
if frame in pose_data and len(pose_data[frame]) >=
|
| 1243 |
keypoints = pose_data[frame]
|
| 1244 |
# Right knee angle using hip, knee, ankle
|
| 1245 |
-
right_hip = np.array(keypoints
|
| 1246 |
-
right_knee = np.array(keypoints
|
| 1247 |
-
right_ankle = np.array(keypoints
|
| 1248 |
|
| 1249 |
# Calculate knee angle
|
| 1250 |
thigh_vector = right_hip - right_knee
|
|
@@ -1255,10 +1206,6 @@ def calculate_biomechanical_metrics(pose_data, swing_phases):
|
|
| 1255 |
cos_angle = np.clip(cos_angle, -1, 1)
|
| 1256 |
knee_angle = np.degrees(np.arccos(cos_angle))
|
| 1257 |
knee_flexions[phase_name] = min(knee_angle, 60)
|
| 1258 |
-
else:
|
| 1259 |
-
knee_flexions[phase_name] = 25
|
| 1260 |
-
else:
|
| 1261 |
-
knee_flexions[phase_name] = 25
|
| 1262 |
|
| 1263 |
metrics["knee_flexion_address"] = knee_flexions.get("address", 25)
|
| 1264 |
metrics["knee_flexion_impact"] = knee_flexions.get("impact", 30)
|
|
@@ -1323,7 +1270,7 @@ def calculate_biomechanical_metrics(pose_data, swing_phases):
|
|
| 1323 |
|
| 1324 |
except Exception as e:
|
| 1325 |
print(f"Error calculating biomechanical metrics: {str(e)}")
|
| 1326 |
-
#
|
| 1327 |
-
|
| 1328 |
|
| 1329 |
return metrics
|
|
|
|
| 392 |
- Energy Transfer: 88.0%, Power Accumulation: 100%, Potential Distance: 286 yards
|
| 393 |
- Sequential Kinematic Sequence: 100%, Swing Plane Consistency: 85%
|
| 394 |
|
| 395 |
+
**Rose Zhang (LPGA Tour Professional):**
|
| 396 |
+
- Hip Rotation: 90°, Shoulder Rotation: 120°, Posture Score: 98.0%
|
| 397 |
+
- Weight Shift: 89.9%, Arm Extension: 79.5%, Wrist Hinge: 112.8°
|
| 398 |
+
- Energy Transfer: 96.6%, Power Accumulation: 100%, Potential Distance: 296 yards
|
| 399 |
+
- Sequential Kinematic Sequence: 100%, Swing Plane Consistency: 85%
|
| 400 |
+
- Speed Generation: Body-dominant
|
| 401 |
+
|
| 402 |
+
**Lydia Ko (LPGA Tour Professional):**
|
| 403 |
+
- Hip Rotation: 90°, Shoulder Rotation: 120°, Posture Score: 99.2%
|
| 404 |
+
- Weight Shift: 66.2%, Arm Extension: 62.1%, Wrist Hinge: 120°
|
| 405 |
+
- Energy Transfer: 88.7%, Power Accumulation: 100%, Potential Distance: 286 yards
|
| 406 |
+
- Sequential Kinematic Sequence: 100%, Swing Plane Consistency: 70%
|
| 407 |
+
- Speed Generation: Body-dominant
|
| 408 |
+
|
| 409 |
### **PROFESSIONAL STANDARDS CALIBRATION (100% Level):**
|
| 410 |
**Core Biomechanical Metrics:**
|
| 411 |
+
- **Hip Rotation**: 25-90° (Professional range - multiple successful approaches)
|
| 412 |
+
- **Shoulder Rotation**: 60-120° (Professional upper body coil range)
|
| 413 |
+
- **Posture Score**: 95-99% (Exceptional spine angle consistency across all professionals)
|
| 414 |
+
- **Weight Shift**: 53-90% (Professional range varies significantly by style)
|
| 415 |
|
| 416 |
**Upper Body Excellence:**
|
| 417 |
+
- **Arm Extension**: 62-100% (Wide professional range - Lydia shows low extension can work)
|
| 418 |
+
- **Wrist Hinge**: 93-120° (Optimal lag and release timing)
|
| 419 |
+
- **Swing Plane Consistency**: 70-85% (Professional-level repeatability)
|
| 420 |
+
- **Chest Rotation Efficiency**: 66-100% (Coordination varies by swing style)
|
| 421 |
|
| 422 |
**Power & Efficiency Markers:**
|
| 423 |
+
- **Energy Transfer Efficiency**: 65-97% (Wide professional range - multiple successful approaches)
|
| 424 |
+
- **Power Accumulation**: 84-100% (Power generation across all styles)
|
| 425 |
+
- **Sequential Kinematic Sequence**: 69-100% (Professional coordination standards)
|
| 426 |
+
- **Potential Distance**: 242-296 yards (Professional power range)
|
| 427 |
|
| 428 |
**Movement Quality Standards:**
|
| 429 |
+
- **Head Movement**: 1-8 inches (Controlled movement varies by professional)
|
| 430 |
+
- **Ground Force Efficiency**: 53-90% (Professional ground interaction range)
|
| 431 |
+
- **Hip Thrust**: 30-100% (Lower body drive varies significantly)
|
| 432 |
|
| 433 |
### **AMATEUR REFERENCE EXAMPLES FOR CALIBRATION:**
|
| 434 |
|
|
|
|
| 441 |
- Head Movement: 8.0in lateral, 6.0in vertical (Excessive movement)
|
| 442 |
- Speed Generation: Mixed
|
| 443 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
**50-60% Level Amateur (Female - Arms-Dominant):**
|
| 445 |
- Hip Rotation: 25°, Shoulder Rotation: 60° (Limited body rotation)
|
| 446 |
- Posture Score: 80.6%, Weight Shift: 50.0% (Needs improvement)
|
|
|
|
| 451 |
- Ground Force: 50.0%, Hip Thrust: 30.0% (Weak lower body)
|
| 452 |
- Speed Generation: Arms-dominant
|
| 453 |
|
| 454 |
+
**CRITICAL INSIGHTS FROM PROFESSIONAL AND AMATEUR ANALYSIS:**
|
| 455 |
+
1. **Hip Rotation Shows Variation**: Professionals range from 63-90°, with moderate rotation (63°) and full rotation (90°) both achieving elite results
|
| 456 |
+
2. **Shoulder Rotation Critical Threshold**: 120° consistently achieved by all professionals, showing this as the elite standard
|
| 457 |
+
3. **Multiple Successful Swing Styles**: Body-dominant swings both achieve elite results with different hip mobility approaches
|
| 458 |
+
4. **Posture Consistency Universal**: All professionals maintain 95-99% posture scores regardless of swing style
|
| 459 |
+
5. **Arm Extension Varies Dramatically**: Professional range 62-100% shows that both high extension (96-100%) and compact swings (62%) can be highly effective
|
| 460 |
+
6. **Energy Transfer Multiple Pathways**: Range from 88-97% in professionals, showing consistent high-level power generation approaches
|
| 461 |
+
7. **Power Accumulation Excellence**: All professionals achieve 100% efficiency, showing this as the elite standard
|
| 462 |
+
8. **Distance Generation Diversity**: Professional distances range 285-296 yards through different mechanical approaches
|
| 463 |
+
9. **Weight Transfer Success Patterns**: Professional range 63-90% shows multiple effective weight shift strategies
|
| 464 |
+
10. **Sequential Timing Excellence**: Professional kinematic sequence consistently at 100%, showing perfect coordination as the standard
|
| 465 |
+
11. **Wrist Hinge Consistency**: Professionals range 93-120°, showing different but effective lag and release strategies
|
| 466 |
+
12. **Ground Force Utilization Excellence**: Range 63-90% with elite players achieving consistent high efficiency through proper lower body mechanics
|
| 467 |
|
| 468 |
## CURRENT SWING ANALYSIS
|
| 469 |
|
|
|
|
| 524 |
|
| 525 |
## ANALYSIS INSTRUCTIONS
|
| 526 |
|
| 527 |
+
**GOLF SWING ANALYSIS FORMAT**
|
| 528 |
+
Use the benchmarks above to guide your evaluation. Follow this exact format:
|
| 529 |
|
| 530 |
+
**PERFORMANCE_CLASSIFICATION:** [XX%]
|
| 531 |
+
(XX = number from 10% to 100%)
|
| 532 |
|
| 533 |
**STRENGTHS:**
|
| 534 |
+
List exactly 3 strengths. Each should:
|
| 535 |
+
- Be qualitative (no numbers)
|
| 536 |
+
- Compare to professional benchmarks
|
| 537 |
+
- Highlight what's working well and when (e.g. during backswing, at impact)
|
| 538 |
+
- Use a positive, supportive tone
|
| 539 |
|
| 540 |
+
Example:
|
| 541 |
+
• Your shoulder rotation during the backswing shows strong upper body mobility, similar to professional swings.
|
|
|
|
|
|
|
| 542 |
|
| 543 |
+
**WEAKNESSES:**
|
| 544 |
+
List exactly 3 areas for improvement. Each should:
|
| 545 |
+
- Use numbers when necessary, and only use 1 number per weakness (for example, the difference between your metric and the professional standard)
|
| 546 |
+
- Describe the impact on power, accuracy, or consistency
|
| 547 |
+
- Use phrases like "less than optimal" or "more than ideal"
|
| 548 |
+
- Don't suggest fixes here—save those for the next section
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
|
| 550 |
+
Example:
|
| 551 |
+
• Your hip rotation is less than optimal, which may reduce your power through the downswing.
|
|
|
|
|
|
|
| 552 |
|
| 553 |
**PRIORITY_IMPROVEMENTS:**
|
| 554 |
+
List exactly 3 improvement areas. Each should:
|
| 555 |
+
- Include the topic name
|
| 556 |
+
- Explain what to improve and when in the swing
|
| 557 |
+
- Reference benchmarks when relevant, without being too technical
|
| 558 |
+
- Use coaching-style language (e.g. "try increasing...")
|
| 559 |
+
- Emphasize benefits
|
| 560 |
+
|
| 561 |
+
Example:
|
| 562 |
+
Hip Mobility: Try increasing your hip rotation during the downswing to unlock more lower body power.
|
| 563 |
+
|
| 564 |
+
**SCORING GUIDELINES (Use to help decide % score)**
|
| 565 |
+
|
| 566 |
+
| Metric | Professional Standard | Note |
|
| 567 |
+
|--------|----------------------|------|
|
| 568 |
+
| Hip Rotation | 25°–90° | <25° is weak |
|
| 569 |
+
| Shoulder Rotation | 60°–120° | <60° is weak |
|
| 570 |
+
| Energy Transfer | 65–97% | <65% = score <60% |
|
| 571 |
+
| Sequential Kinematics | 69–100% | <69% = score <70% |
|
| 572 |
+
| Weight Shift | 53–90% | <53% = weakness |
|
| 573 |
+
| Head Movement | 1–8 in | >8 in = major issue |
|
| 574 |
+
| Arm Extension | 62–100% | <62% = weakness |
|
| 575 |
+
| Power Accumulation | 84–100% | <84% = weakness |
|
| 576 |
+
|
| 577 |
+
**Classification Bands:**
|
| 578 |
+
- **90–100%**: Tour-level
|
| 579 |
+
- **80–89%**: Advanced amateur
|
| 580 |
+
- **70–79%**: Skilled
|
| 581 |
+
- **60–69%**: Intermediate
|
| 582 |
+
- **50–59%**: Developing
|
| 583 |
+
- **40–49%**: Beginner
|
| 584 |
+
- **10–39%**: Novice
|
| 585 |
+
|
| 586 |
+
**STYLE & FORMATTING RULES:**
|
| 587 |
+
- Use these headers: PERFORMANCE_CLASSIFICATION, STRENGTHS, WEAKNESSES, PRIORITY_IMPROVEMENTS
|
| 588 |
+
- Avoid statistics in strengths/weaknesses (okay in improvements if helpful)
|
| 589 |
+
- Tie all points to professional standards
|
| 590 |
+
- Use a positive, coaching tone throughout
|
| 591 |
+
- Avoid saying "perfect" — say "strong" or "meets standards"
|
| 592 |
+
- Focus on biomechanics, not timing (e.g. tempo, frame count)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
"""
|
| 594 |
|
| 595 |
return prompt
|
|
|
|
| 655 |
priority_match = re.search(r'\*\*PRIORITY_IMPROVEMENTS:\*\*\s*(.*?)$', raw_analysis, re.IGNORECASE | re.DOTALL)
|
| 656 |
if priority_match:
|
| 657 |
priority_text = priority_match.group(1)
|
| 658 |
+
# First try to parse numbered format: "1. Topic: Description"
|
| 659 |
+
numbered_items = re.findall(r'(\d+)\.\s*([^1-9\n]*?)(?=\d+\.|$)', priority_text, re.DOTALL)
|
| 660 |
+
|
| 661 |
+
if numbered_items:
|
| 662 |
+
for num, description in numbered_items[:3]: # Limit to 3
|
| 663 |
+
description = description.strip()
|
| 664 |
+
if description and len(description) > 10: # Only add if meaningful content
|
| 665 |
+
formatted_analysis['priority_improvements'].append({
|
| 666 |
+
'rank': int(num),
|
| 667 |
+
'description': description
|
| 668 |
+
})
|
| 669 |
+
else:
|
| 670 |
+
# Try to parse simple format without numbers: "Topic: Description"
|
| 671 |
+
# Split by lines and look for patterns like "Topic: Description"
|
| 672 |
+
lines = [line.strip() for line in priority_text.split('\n') if line.strip()]
|
| 673 |
+
for i, line in enumerate(lines[:3]): # Limit to 3
|
| 674 |
+
if ':' in line and len(line) > 15: # Has colon and meaningful length
|
| 675 |
+
formatted_analysis['priority_improvements'].append({
|
| 676 |
+
'rank': i + 1,
|
| 677 |
+
'description': line
|
| 678 |
+
})
|
| 679 |
+
|
| 680 |
+
# Ensure exactly 3 priority improvements with distinct topics
|
| 681 |
+
if len(formatted_analysis['priority_improvements']) < 3:
|
| 682 |
+
# Define 3 distinct improvement areas
|
| 683 |
+
common_improvements = [
|
| 684 |
+
"Hip Mobility: Try increasing your hip rotation during the downswing to unlock more lower body power and improve overall swing efficiency.",
|
| 685 |
+
"Arm Extension: Focus on achieving better arm extension at impact to improve power transfer and ball striking consistency.",
|
| 686 |
+
"Weight Transfer: Work on shifting your weight more effectively from back foot to front foot during the swing to enhance balance and power generation."
|
| 687 |
+
]
|
| 688 |
+
|
| 689 |
+
# Get existing topics to avoid duplicates
|
| 690 |
+
existing_topics = set()
|
| 691 |
+
for improvement in formatted_analysis['priority_improvements']:
|
| 692 |
+
topic = improvement['description'].split(':')[0].strip().lower()
|
| 693 |
+
existing_topics.add(topic)
|
| 694 |
+
|
| 695 |
+
# Add missing improvements, avoiding duplicates
|
| 696 |
+
current_count = len(formatted_analysis['priority_improvements'])
|
| 697 |
+
for improvement in common_improvements:
|
| 698 |
+
if current_count >= 3:
|
| 699 |
+
break
|
| 700 |
+
topic = improvement.split(':')[0].strip().lower()
|
| 701 |
+
if topic not in existing_topics:
|
| 702 |
+
formatted_analysis['priority_improvements'].append({
|
| 703 |
+
'rank': current_count + 1,
|
| 704 |
+
'description': improvement
|
| 705 |
+
})
|
| 706 |
+
existing_topics.add(topic)
|
| 707 |
+
current_count += 1
|
| 708 |
+
|
| 709 |
+
# Ensure we have exactly 3 (trim if too many)
|
| 710 |
+
formatted_analysis['priority_improvements'] = formatted_analysis['priority_improvements'][:3]
|
| 711 |
+
|
| 712 |
+
# Re-rank to ensure proper numbering
|
| 713 |
+
for i, improvement in enumerate(formatted_analysis['priority_improvements']):
|
| 714 |
+
improvement['rank'] = i + 1
|
| 715 |
|
| 716 |
# Fallback parsing if structured format wasn't used
|
| 717 |
if not formatted_analysis['strengths']:
|
|
|
|
| 783 |
percentage = formatted_analysis['classification']
|
| 784 |
if percentage >= 80:
|
| 785 |
formatted_analysis['priority_improvements'] = [
|
| 786 |
+
{'rank': 1, 'description': 'Technical Refinement: Fine-tune specific mechanics to achieve consistency at the highest level.'},
|
| 787 |
+
{'rank': 2, 'description': 'Performance Optimization: Focus on maximizing efficiency and power transfer.'},
|
| 788 |
+
{'rank': 3, 'description': 'Competitive Preparation: Enhance mental game and course management skills.'}
|
| 789 |
]
|
| 790 |
elif percentage >= 60:
|
| 791 |
formatted_analysis['priority_improvements'] = [
|
| 792 |
+
{'rank': 1, 'description': 'Kinematic Sequence Enhancement: Improve body rotation coordination to generate more power and consistency.'},
|
| 793 |
+
{'rank': 2, 'description': 'Clubface Control: Enhance swing path consistency for better ball striking accuracy.'},
|
| 794 |
+
{'rank': 3, 'description': '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': 'Fundamental Mechanics: Establish consistent posture, grip, and setup positions.'},
|
| 799 |
+
{'rank': 2, 'description': 'Body Rotation Development: Improve hip and shoulder turn coordination.'},
|
| 800 |
+
{'rank': 3, 'description': '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': 'Basic Setup and Posture: Focus on establishing proper spine angle and athletic stance.'},
|
| 805 |
+
{'rank': 2, 'description': 'Fundamental Swing Motion: Develop basic backswing and downswing mechanics.'},
|
| 806 |
+
{'rank': 3, 'description': 'Balance and Stability: Improve overall balance throughout the swing motion.'}
|
| 807 |
]
|
| 808 |
|
| 809 |
return formatted_analysis
|
|
|
|
| 940 |
rank = priority['rank']
|
| 941 |
description = priority['description']
|
| 942 |
|
| 943 |
+
# For simple "Topic: Description" format, just display it cleanly
|
| 944 |
+
if ':' in description:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 945 |
parts = description.split(':', 1)
|
| 946 |
+
topic = parts[0].strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 947 |
desc = parts[1].strip()
|
| 948 |
+
st.markdown(f"**{rank}. {topic}:** {desc}")
|
| 949 |
else:
|
| 950 |
+
# Fallback for other formats
|
| 951 |
+
st.markdown(f"**{rank}. {description}**")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 952 |
|
| 953 |
st.write("") # Add spacing between items
|
| 954 |
|
|
|
|
| 964 |
Returns:
|
| 965 |
dict: Calculated biomechanical metrics
|
| 966 |
"""
|
| 967 |
+
# Initialize default metrics that will be returned even if calculations fail
|
| 968 |
+
metrics = {
|
| 969 |
+
"hip_rotation": 25,
|
| 970 |
+
"shoulder_rotation": 60,
|
| 971 |
+
"weight_shift": 0.5,
|
| 972 |
+
"posture_score": 0.6,
|
| 973 |
+
"arm_extension": 0.6,
|
| 974 |
+
"wrist_hinge": 60,
|
| 975 |
+
"head_movement_lateral": 3.0,
|
| 976 |
+
"head_movement_vertical": 2.0,
|
| 977 |
+
"knee_flexion_address": 25,
|
| 978 |
+
"knee_flexion_impact": 30,
|
| 979 |
+
"swing_plane_consistency": 0.6,
|
| 980 |
+
"chest_rotation_efficiency": 0.6,
|
| 981 |
+
"hip_thrust": 0.5,
|
| 982 |
+
"ground_force_efficiency": 0.6,
|
| 983 |
+
"transition_smoothness": 0.6,
|
| 984 |
+
"kinematic_sequence": 0.6,
|
| 985 |
+
"energy_transfer": 0.6,
|
| 986 |
+
"power_accumulation": 0.6,
|
| 987 |
+
"potential_distance": 200,
|
| 988 |
+
"speed_generation": "Mixed"
|
| 989 |
+
}
|
| 990 |
+
|
| 991 |
+
def safe_get_keypoint(keypoints, index, default_pos=[0.0, 0.0]):
|
| 992 |
+
"""Safely get a keypoint position with bounds checking"""
|
| 993 |
+
try:
|
| 994 |
+
if index < len(keypoints) and keypoints[index] is not None:
|
| 995 |
+
kp = keypoints[index]
|
| 996 |
+
# Handle different keypoint formats
|
| 997 |
+
if isinstance(kp, (list, tuple)) and len(kp) >= 2:
|
| 998 |
+
return [float(kp[0]), float(kp[1])]
|
| 999 |
+
elif hasattr(kp, 'x') and hasattr(kp, 'y'):
|
| 1000 |
+
return [float(kp.x), float(kp.y)]
|
| 1001 |
+
return default_pos
|
| 1002 |
+
except (IndexError, TypeError, AttributeError):
|
| 1003 |
+
return default_pos
|
| 1004 |
|
| 1005 |
# Get key frames for analysis
|
| 1006 |
setup_frames = swing_phases.get("setup", [])
|
|
|
|
| 1027 |
setup_keypoints = pose_data[setup_frame]
|
| 1028 |
backswing_keypoints = pose_data[top_backswing_frame]
|
| 1029 |
|
| 1030 |
+
if len(setup_keypoints) >= 25 and len(backswing_keypoints) >= 25:
|
| 1031 |
# Hip rotation calculation using hip landmarks
|
| 1032 |
+
setup_left_hip = np.array(safe_get_keypoint(setup_keypoints, 23))
|
| 1033 |
+
setup_right_hip = np.array(safe_get_keypoint(setup_keypoints, 24))
|
| 1034 |
+
backswing_left_hip = np.array(safe_get_keypoint(backswing_keypoints, 23))
|
| 1035 |
+
backswing_right_hip = np.array(safe_get_keypoint(backswing_keypoints, 24))
|
| 1036 |
|
| 1037 |
# Calculate hip line angles
|
| 1038 |
setup_hip_vector = setup_right_hip - setup_left_hip
|
| 1039 |
backswing_hip_vector = backswing_right_hip - backswing_left_hip
|
| 1040 |
|
| 1041 |
+
if np.linalg.norm(setup_hip_vector) > 0 and np.linalg.norm(backswing_hip_vector) > 0:
|
| 1042 |
+
setup_hip_angle = np.degrees(np.arctan2(setup_hip_vector[1], setup_hip_vector[0]))
|
| 1043 |
+
backswing_hip_angle = np.degrees(np.arctan2(backswing_hip_vector[1], backswing_hip_vector[0]))
|
| 1044 |
+
|
| 1045 |
+
hip_rotation = abs(backswing_hip_angle - setup_hip_angle)
|
| 1046 |
+
# Normalize to reasonable range (professionals typically achieve 45+ degrees)
|
| 1047 |
+
metrics["hip_rotation"] = min(hip_rotation, 90)
|
|
|
|
|
|
|
|
|
|
| 1048 |
|
| 1049 |
# Calculate Shoulder Rotation
|
| 1050 |
if setup_frame and top_backswing_frame and setup_frame in pose_data and top_backswing_frame in pose_data:
|
| 1051 |
setup_keypoints = pose_data[setup_frame]
|
| 1052 |
backswing_keypoints = pose_data[top_backswing_frame]
|
| 1053 |
|
| 1054 |
+
if len(setup_keypoints) >= 13 and len(backswing_keypoints) >= 13:
|
| 1055 |
# Shoulder rotation calculation
|
| 1056 |
+
setup_left_shoulder = np.array(safe_get_keypoint(setup_keypoints, 11))
|
| 1057 |
+
setup_right_shoulder = np.array(safe_get_keypoint(setup_keypoints, 12))
|
| 1058 |
+
backswing_left_shoulder = np.array(safe_get_keypoint(backswing_keypoints, 11))
|
| 1059 |
+
backswing_right_shoulder = np.array(safe_get_keypoint(backswing_keypoints, 12))
|
| 1060 |
|
| 1061 |
setup_shoulder_vector = setup_right_shoulder - setup_left_shoulder
|
| 1062 |
backswing_shoulder_vector = backswing_right_shoulder - backswing_left_shoulder
|
| 1063 |
|
| 1064 |
+
if np.linalg.norm(setup_shoulder_vector) > 0 and np.linalg.norm(backswing_shoulder_vector) > 0:
|
| 1065 |
+
setup_shoulder_angle = np.degrees(np.arctan2(setup_shoulder_vector[1], setup_shoulder_vector[0]))
|
| 1066 |
+
backswing_shoulder_angle = np.degrees(np.arctan2(backswing_shoulder_vector[1], backswing_shoulder_vector[0]))
|
| 1067 |
+
|
| 1068 |
+
shoulder_rotation = abs(backswing_shoulder_angle - setup_shoulder_angle)
|
| 1069 |
+
metrics["shoulder_rotation"] = min(shoulder_rotation, 120)
|
|
|
|
|
|
|
|
|
|
| 1070 |
|
| 1071 |
# Calculate Weight Shift (using hip and ankle positions)
|
| 1072 |
if setup_frame and impact_frame and setup_frame in pose_data and impact_frame in pose_data:
|
| 1073 |
setup_keypoints = pose_data[setup_frame]
|
| 1074 |
impact_keypoints = pose_data[impact_frame]
|
| 1075 |
|
| 1076 |
+
if len(setup_keypoints) >= 29 and len(impact_keypoints) >= 29:
|
| 1077 |
# Use center of mass approximation
|
| 1078 |
+
setup_left_ankle = np.array(safe_get_keypoint(setup_keypoints, 27))
|
| 1079 |
+
setup_right_ankle = np.array(safe_get_keypoint(setup_keypoints, 28))
|
| 1080 |
+
impact_left_ankle = np.array(safe_get_keypoint(impact_keypoints, 27))
|
| 1081 |
+
impact_right_ankle = np.array(safe_get_keypoint(impact_keypoints, 28))
|
| 1082 |
|
| 1083 |
# Calculate weight distribution based on foot positioning
|
| 1084 |
setup_center = (setup_left_ankle + setup_right_ankle) / 2
|
|
|
|
| 1090 |
weight_shift_amount = np.linalg.norm(impact_center - setup_center) / foot_width
|
| 1091 |
# Convert to percentage (professionals typically achieve 70%+ to front foot)
|
| 1092 |
weight_shift = min(0.5 + weight_shift_amount * 0.5, 0.9)
|
| 1093 |
+
metrics["weight_shift"] = weight_shift
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1094 |
|
| 1095 |
# Calculate Posture Score (spine angle consistency)
|
| 1096 |
posture_scores = []
|
| 1097 |
for frame_list in [setup_frames, backswing_frames, impact_frames]:
|
| 1098 |
if frame_list:
|
| 1099 |
frame = frame_list[len(frame_list) // 2]
|
| 1100 |
+
if frame in pose_data and len(pose_data[frame]) >= 25:
|
| 1101 |
keypoints = pose_data[frame]
|
| 1102 |
# Use shoulder and hip landmarks to estimate spine angle
|
| 1103 |
+
left_shoulder = np.array(safe_get_keypoint(keypoints, 11))
|
| 1104 |
+
right_shoulder = np.array(safe_get_keypoint(keypoints, 12))
|
| 1105 |
+
left_hip = np.array(safe_get_keypoint(keypoints, 23))
|
| 1106 |
+
right_hip = np.array(safe_get_keypoint(keypoints, 24))
|
| 1107 |
|
| 1108 |
shoulder_center = (left_shoulder + right_shoulder) / 2
|
| 1109 |
hip_center = (left_hip + right_hip) / 2
|
| 1110 |
|
| 1111 |
spine_vector = shoulder_center - hip_center
|
| 1112 |
+
if np.linalg.norm(spine_vector) > 0:
|
| 1113 |
+
spine_angle = np.degrees(np.arctan2(spine_vector[1], spine_vector[0]))
|
| 1114 |
+
posture_scores.append(abs(spine_angle))
|
| 1115 |
|
| 1116 |
if posture_scores:
|
| 1117 |
# Good posture = consistent spine angle across phases
|
| 1118 |
posture_consistency = 1.0 - (np.std(posture_scores) / 90.0) # Normalize by 90 degrees
|
| 1119 |
metrics["posture_score"] = max(0.3, min(posture_consistency, 1.0))
|
|
|
|
|
|
|
| 1120 |
|
| 1121 |
# Calculate Arm Extension at Impact
|
| 1122 |
+
if impact_frame and impact_frame in pose_data and len(pose_data[impact_frame]) >= 17:
|
| 1123 |
keypoints = pose_data[impact_frame]
|
| 1124 |
+
right_shoulder = np.array(safe_get_keypoint(keypoints, 12))
|
| 1125 |
+
right_elbow = np.array(safe_get_keypoint(keypoints, 14))
|
| 1126 |
+
right_wrist = np.array(safe_get_keypoint(keypoints, 16))
|
| 1127 |
|
| 1128 |
# Calculate arm extension
|
| 1129 |
upper_arm = np.linalg.norm(right_elbow - right_shoulder)
|
|
|
|
| 1136 |
if total_arm_length > 0:
|
| 1137 |
extension_ratio = actual_distance / total_arm_length
|
| 1138 |
metrics["arm_extension"] = min(extension_ratio, 1.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1139 |
|
| 1140 |
# Calculate Wrist Hinge using joint angles
|
| 1141 |
wrist_angles = []
|
|
|
|
| 1143 |
if frame_list:
|
| 1144 |
frame = frame_list[len(frame_list) // 2]
|
| 1145 |
if frame in pose_data:
|
| 1146 |
+
try:
|
| 1147 |
+
angles = calculate_joint_angles(pose_data[frame])
|
| 1148 |
+
if angles and "right_wrist" in angles:
|
| 1149 |
+
wrist_angles.append(angles["right_wrist"])
|
| 1150 |
+
except Exception:
|
| 1151 |
+
pass # Skip if joint angle calculation fails
|
| 1152 |
|
| 1153 |
if wrist_angles:
|
| 1154 |
avg_wrist_angle = np.mean(wrist_angles)
|
| 1155 |
# Good wrist hinge is typically 80+ degrees
|
| 1156 |
metrics["wrist_hinge"] = min(avg_wrist_angle, 120)
|
|
|
|
|
|
|
| 1157 |
|
| 1158 |
# Calculate Head Movement (lateral and vertical)
|
| 1159 |
if setup_frame and impact_frame and setup_frame in pose_data and impact_frame in pose_data:
|
| 1160 |
setup_keypoints = pose_data[setup_frame]
|
| 1161 |
impact_keypoints = pose_data[impact_frame]
|
| 1162 |
|
| 1163 |
+
if len(setup_keypoints) >= 1 and len(impact_keypoints) >= 1:
|
| 1164 |
# Use nose landmark (index 0) for head position
|
| 1165 |
+
setup_head = np.array(safe_get_keypoint(setup_keypoints, 0))
|
| 1166 |
+
impact_head = np.array(safe_get_keypoint(impact_keypoints, 0))
|
| 1167 |
|
| 1168 |
head_movement = np.abs(impact_head - setup_head)
|
| 1169 |
# Convert pixel movement to approximate inches (rough estimation)
|
| 1170 |
# Assume average person's head is about 9 inches, use that as scale
|
| 1171 |
if len(setup_keypoints) > 10: # Have enough landmarks
|
| 1172 |
+
mouth_pos = safe_get_keypoint(setup_keypoints, 10)
|
| 1173 |
+
head_height_pixels = abs(setup_head[1] - mouth_pos[1])
|
| 1174 |
if head_height_pixels > 0:
|
| 1175 |
pixel_to_inch = 4.0 / head_height_pixels # Approximate nose-to-mouth is 4 inches
|
| 1176 |
lateral_movement = head_movement[0] * pixel_to_inch
|
|
|
|
| 1184 |
|
| 1185 |
metrics["head_movement_lateral"] = min(lateral_movement, 8.0)
|
| 1186 |
metrics["head_movement_vertical"] = min(vertical_movement, 6.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1187 |
|
| 1188 |
# Calculate Knee Flexion
|
| 1189 |
knee_flexions = {}
|
| 1190 |
for phase_name, frame_list in [("address", setup_frames), ("impact", impact_frames)]:
|
| 1191 |
if frame_list:
|
| 1192 |
frame = frame_list[len(frame_list) // 2]
|
| 1193 |
+
if frame in pose_data and len(pose_data[frame]) >= 29:
|
| 1194 |
keypoints = pose_data[frame]
|
| 1195 |
# Right knee angle using hip, knee, ankle
|
| 1196 |
+
right_hip = np.array(safe_get_keypoint(keypoints, 24))
|
| 1197 |
+
right_knee = np.array(safe_get_keypoint(keypoints, 26))
|
| 1198 |
+
right_ankle = np.array(safe_get_keypoint(keypoints, 28))
|
| 1199 |
|
| 1200 |
# Calculate knee angle
|
| 1201 |
thigh_vector = right_hip - right_knee
|
|
|
|
| 1206 |
cos_angle = np.clip(cos_angle, -1, 1)
|
| 1207 |
knee_angle = np.degrees(np.arccos(cos_angle))
|
| 1208 |
knee_flexions[phase_name] = min(knee_angle, 60)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1209 |
|
| 1210 |
metrics["knee_flexion_address"] = knee_flexions.get("address", 25)
|
| 1211 |
metrics["knee_flexion_impact"] = knee_flexions.get("impact", 30)
|
|
|
|
| 1270 |
|
| 1271 |
except Exception as e:
|
| 1272 |
print(f"Error calculating biomechanical metrics: {str(e)}")
|
| 1273 |
+
# Don't return None - instead return the default metrics that were initialized
|
| 1274 |
+
pass
|
| 1275 |
|
| 1276 |
return metrics
|
app/streamlit_app.py
CHANGED
|
@@ -12,6 +12,7 @@ from pathlib import Path
|
|
| 12 |
import shutil
|
| 13 |
import cv2
|
| 14 |
from PIL import Image
|
|
|
|
| 15 |
|
| 16 |
# Load environment variables
|
| 17 |
load_dotenv()
|
|
@@ -27,12 +28,440 @@ from app.models.llm_analyzer import generate_swing_analysis, create_llm_prompt,
|
|
| 27 |
from app.utils.visualizer import create_annotated_video
|
| 28 |
from app.utils.comparison import create_key_frame_comparison, extract_key_swing_frames
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
# Set page config
|
| 31 |
st.set_page_config(page_title="Par-ity Project: Golf Swing Analysis 🏌️♀️",
|
| 32 |
page_icon="🏌️♀️",
|
| 33 |
layout="wide",
|
| 34 |
initial_sidebar_state="collapsed")
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
# Define functions
|
| 38 |
def validate_youtube_url(url):
|
|
@@ -106,7 +535,9 @@ def main():
|
|
| 106 |
'trajectory_data': None,
|
| 107 |
'sample_rate': None
|
| 108 |
}
|
| 109 |
-
|
|
|
|
|
|
|
| 110 |
# Add session cleanup - clean up old files when starting a new session
|
| 111 |
if 'session_initialized' not in st.session_state:
|
| 112 |
cleanup_result = cleanup_downloads_directory(keep_annotated=True)
|
|
@@ -237,13 +668,12 @@ def main():
|
|
| 237 |
'prompt': prompt
|
| 238 |
}
|
| 239 |
|
| 240 |
-
#
|
| 241 |
-
|
| 242 |
-
cleanup_video_file(video_path)
|
| 243 |
|
| 244 |
# Present the options after analysis
|
| 245 |
st.subheader("What would you like to do next?")
|
| 246 |
-
options_col1, options_col2, options_col3 = st.columns(
|
| 247 |
|
| 248 |
with options_col1:
|
| 249 |
st.info(
|
|
@@ -259,6 +689,11 @@ def main():
|
|
| 259 |
st.info(
|
| 260 |
"**Option 3: Key Frame Analysis**\n\nExtract and review your setup, top of backswing, and impact frames with helpful comments for each phase."
|
| 261 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
except Exception as e:
|
| 264 |
st.error(f"Error during analysis: {str(e)}")
|
|
@@ -276,7 +711,7 @@ def main():
|
|
| 276 |
language="text")
|
| 277 |
|
| 278 |
# Create columns for the action buttons
|
| 279 |
-
button_col1, button_col2, button_col3 = st.columns(
|
| 280 |
|
| 281 |
with button_col1:
|
| 282 |
annotated_video_clicked = st.button("Generate Annotated Video",
|
|
@@ -292,9 +727,16 @@ def main():
|
|
| 292 |
keyframe_analysis_clicked = st.button("Key Frame Analysis",
|
| 293 |
key="keyframe_analysis",
|
| 294 |
use_container_width=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
# Handle annotated video creation
|
| 297 |
if annotated_video_clicked:
|
|
|
|
|
|
|
| 298 |
try:
|
| 299 |
with st.spinner("Creating annotated video..."):
|
| 300 |
# Create downloads directory if it doesn't exist
|
|
@@ -341,6 +783,8 @@ def main():
|
|
| 341 |
|
| 342 |
# Handle improvement recommendations generation
|
| 343 |
if improvements_clicked:
|
|
|
|
|
|
|
| 344 |
with st.spinner(
|
| 345 |
"Analyzing your swing and generating recommendations..."):
|
| 346 |
# Get data from session state
|
|
@@ -383,8 +827,11 @@ def main():
|
|
| 383 |
else:
|
| 384 |
# Show error message if analysis failed
|
| 385 |
st.error(analysis)
|
|
|
|
| 386 |
# Handle key frame analysis (new tab/option)
|
| 387 |
if keyframe_analysis_clicked:
|
|
|
|
|
|
|
| 388 |
try:
|
| 389 |
with st.spinner("Extracting key frames from your swing..."):
|
| 390 |
user_video_path = st.session_state.analysis_data['video_path']
|
|
@@ -479,6 +926,23 @@ def main():
|
|
| 479 |
except Exception as e:
|
| 480 |
st.error(f"Error during key frame analysis: {str(e)}")
|
| 481 |
st.info("Please ensure your video is in a supported format and try again.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
|
| 483 |
|
| 484 |
if __name__ == "__main__":
|
|
|
|
| 12 |
import shutil
|
| 13 |
import cv2
|
| 14 |
from PIL import Image
|
| 15 |
+
from datetime import datetime
|
| 16 |
|
| 17 |
# Load environment variables
|
| 18 |
load_dotenv()
|
|
|
|
| 28 |
from app.utils.visualizer import create_annotated_video
|
| 29 |
from app.utils.comparison import create_key_frame_comparison, extract_key_swing_frames
|
| 30 |
|
| 31 |
+
# Import RAG functionality
|
| 32 |
+
try:
|
| 33 |
+
from app.golf_swing_rag import GolfSwingRAG
|
| 34 |
+
RAG_AVAILABLE = True
|
| 35 |
+
except ImportError:
|
| 36 |
+
RAG_AVAILABLE = False
|
| 37 |
+
st.warning("RAG functionality not available. Please ensure golf_swing_rag.py is in the app directory.")
|
| 38 |
+
|
| 39 |
# Set page config
|
| 40 |
st.set_page_config(page_title="Par-ity Project: Golf Swing Analysis 🏌️♀️",
|
| 41 |
page_icon="🏌️♀️",
|
| 42 |
layout="wide",
|
| 43 |
initial_sidebar_state="collapsed")
|
| 44 |
|
| 45 |
+
# Custom CSS for RAG interface
|
| 46 |
+
st.markdown("""
|
| 47 |
+
<style>
|
| 48 |
+
.chat-message {
|
| 49 |
+
padding: 1rem;
|
| 50 |
+
border-radius: 10px;
|
| 51 |
+
margin: 1rem 0;
|
| 52 |
+
}
|
| 53 |
+
.user-message {
|
| 54 |
+
background-color: #e3f2fd;
|
| 55 |
+
border-left: 4px solid #2196f3;
|
| 56 |
+
}
|
| 57 |
+
.assistant-message {
|
| 58 |
+
background-color: #f1f8e9;
|
| 59 |
+
border-left: 4px solid #4caf50;
|
| 60 |
+
}
|
| 61 |
+
.rag-header {
|
| 62 |
+
color: #2E8B57;
|
| 63 |
+
font-size: 1.5rem;
|
| 64 |
+
font-weight: bold;
|
| 65 |
+
margin-bottom: 1rem;
|
| 66 |
+
}
|
| 67 |
+
</style>
|
| 68 |
+
""", unsafe_allow_html=True)
|
| 69 |
+
|
| 70 |
+
@st.cache_resource
|
| 71 |
+
def load_rag_system():
|
| 72 |
+
"""Load and initialize the RAG system (cached for performance)"""
|
| 73 |
+
if not RAG_AVAILABLE:
|
| 74 |
+
return None
|
| 75 |
+
try:
|
| 76 |
+
with st.spinner("Loading golf swing knowledge base..."):
|
| 77 |
+
rag = GolfSwingRAG()
|
| 78 |
+
rag.load_and_process_data()
|
| 79 |
+
rag.create_embeddings()
|
| 80 |
+
return rag
|
| 81 |
+
except Exception as e:
|
| 82 |
+
st.error(f"Error loading RAG system: {str(e)}")
|
| 83 |
+
return None
|
| 84 |
+
|
| 85 |
+
def display_rag_sources(sources):
|
| 86 |
+
"""Display source information in an organized way"""
|
| 87 |
+
if not sources:
|
| 88 |
+
return
|
| 89 |
+
|
| 90 |
+
st.subheader("📚 Sources")
|
| 91 |
+
for i, source in enumerate(sources[:3]): # Show top 3 sources
|
| 92 |
+
with st.expander(f"Source {i+1}: {source['metadata']['title'][:60]}..."):
|
| 93 |
+
st.write(f"**Similarity Score:** {source['similarity_score']:.3f}")
|
| 94 |
+
st.write(f"**Source:** {source['metadata']['source']}")
|
| 95 |
+
if source['metadata']['url']:
|
| 96 |
+
st.write(f"**URL:** [Link]({source['metadata']['url']})")
|
| 97 |
+
st.write("**Content:**")
|
| 98 |
+
st.write(source['chunk'][:500] + "..." if len(source['chunk']) > 500 else source['chunk'])
|
| 99 |
+
|
| 100 |
+
def render_rag_interface():
|
| 101 |
+
"""Render the RAG chatbot interface"""
|
| 102 |
+
# Removed header and description
|
| 103 |
+
|
| 104 |
+
# Initialize RAG system
|
| 105 |
+
if 'rag_system' not in st.session_state and RAG_AVAILABLE:
|
| 106 |
+
st.session_state.rag_system = load_rag_system()
|
| 107 |
+
|
| 108 |
+
# Initialize chat history if not exists
|
| 109 |
+
if 'rag_chat_history' not in st.session_state:
|
| 110 |
+
st.session_state.rag_chat_history = []
|
| 111 |
+
|
| 112 |
+
if not RAG_AVAILABLE or st.session_state.get('rag_system') is None:
|
| 113 |
+
st.error("RAG system is not available. Please check the setup.")
|
| 114 |
+
return
|
| 115 |
+
|
| 116 |
+
# Check if we have video analysis data to enhance responses
|
| 117 |
+
user_swing_context = ""
|
| 118 |
+
if st.session_state.get('video_analyzed') and 'analysis_data' in st.session_state:
|
| 119 |
+
stored_data = st.session_state.analysis_data
|
| 120 |
+
|
| 121 |
+
# Use the structured analysis_data instead of just the prompt
|
| 122 |
+
if 'analysis_data' in stored_data:
|
| 123 |
+
structured_analysis = stored_data['analysis_data']
|
| 124 |
+
|
| 125 |
+
# Format the structured data for better RAG context
|
| 126 |
+
user_swing_context = f"""
|
| 127 |
+
|
| 128 |
+
USER'S SWING ANALYSIS:
|
| 129 |
+
|
| 130 |
+
=== SWING TIMING & PHASES ===
|
| 131 |
+
Swing Phases:
|
| 132 |
+
- Setup: {structured_analysis.get('swing_phases', {}).get('setup', {}).get('frame_count', 0)} frames
|
| 133 |
+
- Backswing: {structured_analysis.get('swing_phases', {}).get('backswing', {}).get('frame_count', 0)} frames
|
| 134 |
+
- Downswing: {structured_analysis.get('swing_phases', {}).get('downswing', {}).get('frame_count', 0)} frames
|
| 135 |
+
- Impact: {structured_analysis.get('swing_phases', {}).get('impact', {}).get('frame_count', 0)} frames
|
| 136 |
+
- Follow-through: {structured_analysis.get('swing_phases', {}).get('follow_through', {}).get('frame_count', 0)} frames
|
| 137 |
+
|
| 138 |
+
Timing Metrics:
|
| 139 |
+
- Tempo Ratio (down:back): {structured_analysis.get('timing_metrics', {}).get('tempo_ratio', 'N/A')}
|
| 140 |
+
- Estimated Club Speed: {structured_analysis.get('timing_metrics', {}).get('estimated_club_speed_mph', 'N/A')} mph
|
| 141 |
+
- Total Swing Time: {structured_analysis.get('timing_metrics', {}).get('total_swing_time_ms', 'N/A')} ms
|
| 142 |
+
|
| 143 |
+
=== BIOMECHANICAL METRICS ===
|
| 144 |
+
Core Body Mechanics:
|
| 145 |
+
- Hip Rotation: {structured_analysis.get('biomechanical_metrics', {}).get('hip_rotation_degrees', 'N/A')}°
|
| 146 |
+
- Shoulder Rotation: {structured_analysis.get('biomechanical_metrics', {}).get('shoulder_rotation_degrees', 'N/A')}°
|
| 147 |
+
- Posture Score: {structured_analysis.get('biomechanical_metrics', {}).get('posture_score_percent', 'N/A')}%
|
| 148 |
+
- Weight Shift: {structured_analysis.get('biomechanical_metrics', {}).get('weight_shift_percent', 'N/A')}%
|
| 149 |
+
|
| 150 |
+
Upper Body Mechanics:
|
| 151 |
+
- Arm Extension: {structured_analysis.get('biomechanical_metrics', {}).get('arm_extension_percent', 'N/A')}%
|
| 152 |
+
- Wrist Hinge: {structured_analysis.get('biomechanical_metrics', {}).get('wrist_hinge_degrees', 'N/A')}°
|
| 153 |
+
- Swing Plane Consistency: {structured_analysis.get('biomechanical_metrics', {}).get('swing_plane_consistency_percent', 'N/A')}%
|
| 154 |
+
- Head Movement (lateral): {structured_analysis.get('biomechanical_metrics', {}).get('head_movement_lateral_inches', 'N/A')} in
|
| 155 |
+
- Head Movement (vertical): {structured_analysis.get('biomechanical_metrics', {}).get('head_movement_vertical_inches', 'N/A')} in
|
| 156 |
+
|
| 157 |
+
Lower Body Mechanics:
|
| 158 |
+
- Hip Thrust: {structured_analysis.get('biomechanical_metrics', {}).get('hip_thrust_percent', 'N/A')}%
|
| 159 |
+
- Ground Force Efficiency: {structured_analysis.get('biomechanical_metrics', {}).get('ground_force_efficiency_percent', 'N/A')}%
|
| 160 |
+
- Knee Flexion (address): {structured_analysis.get('biomechanical_metrics', {}).get('knee_flexion_address_degrees', 'N/A')}°
|
| 161 |
+
- Knee Flexion (impact): {structured_analysis.get('biomechanical_metrics', {}).get('knee_flexion_impact_degrees', 'N/A')}°
|
| 162 |
+
|
| 163 |
+
Movement Quality & Coordination:
|
| 164 |
+
- Sequential Kinematic Sequence: {structured_analysis.get('biomechanical_metrics', {}).get('kinematic_sequence_percent', 'N/A')}%
|
| 165 |
+
- Energy Transfer Efficiency: {structured_analysis.get('biomechanical_metrics', {}).get('energy_transfer_efficiency_percent', 'N/A')}%
|
| 166 |
+
- Power Accumulation: {structured_analysis.get('biomechanical_metrics', {}).get('power_accumulation_percent', 'N/A')}%
|
| 167 |
+
- Transition Smoothness: {structured_analysis.get('biomechanical_metrics', {}).get('transition_smoothness_percent', 'N/A')}%
|
| 168 |
+
|
| 169 |
+
Performance Estimates:
|
| 170 |
+
- Potential Distance: {structured_analysis.get('biomechanical_metrics', {}).get('potential_distance_yards', 'N/A')} yards
|
| 171 |
+
- Speed Generation Method: {structured_analysis.get('biomechanical_metrics', {}).get('speed_generation_method', 'N/A')}
|
| 172 |
+
|
| 173 |
+
=== TRAJECTORY ANALYSIS ===
|
| 174 |
+
- Estimated Carry Distance: {structured_analysis.get('trajectory_analysis', {}).get('estimated_carry_distance', 'N/A')} yards
|
| 175 |
+
- Estimated Ball Speed: {structured_analysis.get('trajectory_analysis', {}).get('estimated_ball_speed', 'N/A')} mph
|
| 176 |
+
- Trajectory Type: {structured_analysis.get('trajectory_analysis', {}).get('trajectory_type', 'N/A')}
|
| 177 |
+
"""
|
| 178 |
+
|
| 179 |
+
# Removed success message
|
| 180 |
+
elif 'prompt' in stored_data:
|
| 181 |
+
# Fallback to prompt if structured data not available
|
| 182 |
+
user_swing_context = f"\n\nUSER'S SWING ANALYSIS:\n{stored_data['prompt']}"
|
| 183 |
+
# Removed success message
|
| 184 |
+
|
| 185 |
+
# Create columns for layout
|
| 186 |
+
col1, col2 = st.columns([2, 1])
|
| 187 |
+
|
| 188 |
+
with col1:
|
| 189 |
+
# Removed subheader
|
| 190 |
+
|
| 191 |
+
# Question input (removed label)
|
| 192 |
+
question = st.text_area(
|
| 193 |
+
"", # Removed label
|
| 194 |
+
height=100,
|
| 195 |
+
placeholder="Ask about your golf swing technique..."
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
# Removed settings section - using smart defaults instead
|
| 199 |
+
|
| 200 |
+
col_submit, col_clear = st.columns([1, 1])
|
| 201 |
+
with col_submit:
|
| 202 |
+
submit_button = st.button("🎯 Get Answer", type="primary", use_container_width=True)
|
| 203 |
+
with col_clear:
|
| 204 |
+
if st.button("🗑️ Clear Chat History", use_container_width=True):
|
| 205 |
+
st.session_state.rag_chat_history = []
|
| 206 |
+
# Don't call st.rerun() here to avoid disappearing interface
|
| 207 |
+
st.success("Chat history cleared!")
|
| 208 |
+
|
| 209 |
+
# Process question
|
| 210 |
+
if submit_button and question.strip():
|
| 211 |
+
with st.spinner("Analyzing your question and searching the knowledge base..."):
|
| 212 |
+
try:
|
| 213 |
+
# Enhanced query method that includes user's swing context
|
| 214 |
+
# Use smart default for number of sources (3-5 depending on context)
|
| 215 |
+
num_sources = 5 if user_swing_context else 3 # More sources when we have swing analysis
|
| 216 |
+
result = query_with_user_context(
|
| 217 |
+
st.session_state.rag_system,
|
| 218 |
+
question,
|
| 219 |
+
user_swing_context,
|
| 220 |
+
top_k=num_sources
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
# Add to chat history
|
| 224 |
+
st.session_state.rag_chat_history.append({
|
| 225 |
+
'question': question,
|
| 226 |
+
'response': result['response'],
|
| 227 |
+
'sources': result['sources'],
|
| 228 |
+
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 229 |
+
'used_swing_context': bool(user_swing_context)
|
| 230 |
+
})
|
| 231 |
+
|
| 232 |
+
st.success("Answer generated successfully!")
|
| 233 |
+
|
| 234 |
+
except Exception as e:
|
| 235 |
+
st.error(f"An error occurred: {str(e)}")
|
| 236 |
+
|
| 237 |
+
# Display chat history (simplified)
|
| 238 |
+
if st.session_state.rag_chat_history:
|
| 239 |
+
for i, chat in enumerate(reversed(st.session_state.rag_chat_history)):
|
| 240 |
+
# Removed question numbers, timestamps, and personalization indicators
|
| 241 |
+
|
| 242 |
+
# Question
|
| 243 |
+
st.markdown(f'<div class="chat-message user-message"><strong>🤔 Your Question:</strong><br>{chat["question"]}</div>',
|
| 244 |
+
unsafe_allow_html=True)
|
| 245 |
+
|
| 246 |
+
# Response
|
| 247 |
+
st.markdown(f'<div class="chat-message assistant-message"><strong>⛳ Expert Answer:</strong><br>{chat["response"]}</div>',
|
| 248 |
+
unsafe_allow_html=True)
|
| 249 |
+
|
| 250 |
+
# Removed sources display
|
| 251 |
+
|
| 252 |
+
st.divider()
|
| 253 |
+
|
| 254 |
+
with col2:
|
| 255 |
+
# Removed all the About section, Tips, Personalized Questions, and metrics
|
| 256 |
+
pass
|
| 257 |
+
|
| 258 |
+
def query_with_user_context(rag_system, question, user_swing_context, top_k=5):
|
| 259 |
+
"""Enhanced query method that includes user's swing analysis context"""
|
| 260 |
+
# Search for relevant chunks
|
| 261 |
+
relevant_chunks = rag_system.search_similar_chunks(question, top_k)
|
| 262 |
+
|
| 263 |
+
# Generate response with enhanced context
|
| 264 |
+
response = generate_enhanced_response(rag_system, question, relevant_chunks, user_swing_context)
|
| 265 |
+
print(f"Response: {response}")
|
| 266 |
+
|
| 267 |
+
return {
|
| 268 |
+
'response': response,
|
| 269 |
+
'sources': relevant_chunks,
|
| 270 |
+
'query': question,
|
| 271 |
+
'timestamp': datetime.now().isoformat()
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
def generate_enhanced_response(rag_system, query, context_chunks, user_swing_context=""):
|
| 275 |
+
"""Generate response using OpenAI API with user's swing analysis as the main system prompt"""
|
| 276 |
+
if not rag_system.openai_client:
|
| 277 |
+
print("No OpenAI client found")
|
| 278 |
+
return generate_enhanced_fallback_response(query, context_chunks, user_swing_context)
|
| 279 |
+
|
| 280 |
+
# Prepare context from knowledge base
|
| 281 |
+
knowledge_context = "\n\n".join([f"Reference Material from '{chunk['metadata']['title']}':\n{chunk['chunk']}"
|
| 282 |
+
for chunk in context_chunks])
|
| 283 |
+
|
| 284 |
+
# Use the user's swing analysis as the primary system prompt if available
|
| 285 |
+
print(f"User swing context: {user_swing_context}")
|
| 286 |
+
if user_swing_context:
|
| 287 |
+
# Extract the actual analysis content (remove the header)
|
| 288 |
+
analysis_content = user_swing_context.replace("USER'S SWING ANALYSIS:\n", "").strip()
|
| 289 |
+
|
| 290 |
+
system_prompt = f"""{analysis_content}
|
| 291 |
+
|
| 292 |
+
You are a golf swing technique expert assistant analyzing this specific player's swing.
|
| 293 |
+
|
| 294 |
+
IMPORTANT: Only reference the player's swing analysis data above if the question is directly related to swing motion biomechanics (like hip rotation, shoulder turn, weight transfer, timing, etc.).
|
| 295 |
+
|
| 296 |
+
Do NOT reference swing analysis for questions about:
|
| 297 |
+
- Grip (how to hold the club)
|
| 298 |
+
- Setup/stance (static positioning before the swing)
|
| 299 |
+
- Equipment (clubs, balls, etc.)
|
| 300 |
+
- Course management
|
| 301 |
+
- Mental game
|
| 302 |
+
- Basic fundamentals that aren't measured during swing motion
|
| 303 |
+
|
| 304 |
+
Follow this response structure:
|
| 305 |
+
|
| 306 |
+
1. Synthesize information from the reference materials below to answer the user's question. Keep this to 2-4 sentences maximum. Start with "Based on [source name]," and provide clear, actionable advice about the technique.
|
| 307 |
+
|
| 308 |
+
2. If the question relates to swing motion biomechanics AND you found relevant measurements in the analysis above, provide specific improvement advice comparing current state to recommendations. Otherwise, provide general advice without forcing connections to unrelated swing metrics.
|
| 309 |
+
|
| 310 |
+
Reference Materials from Golf Instruction Database:
|
| 311 |
+
{knowledge_context}"""
|
| 312 |
+
|
| 313 |
+
user_prompt = f"""Based on the golf instruction reference materials provided, please answer this question about golf swing technique:
|
| 314 |
+
|
| 315 |
+
{query}
|
| 316 |
+
|
| 317 |
+
Remember to:
|
| 318 |
+
1. Only reference my swing analysis if the question is about swing motion biomechanics
|
| 319 |
+
2. Synthesize expert advice concisely (2-4 sentences max)
|
| 320 |
+
3. Don't force connections between unrelated topics (e.g., don't mention wrist hinge when asking about grip)"""
|
| 321 |
+
|
| 322 |
+
else:
|
| 323 |
+
# Fallback to general system prompt if no swing analysis available
|
| 324 |
+
system_prompt = f"""You are a golf swing technique expert assistant. You help golfers improve their swing by providing detailed, accurate advice based on professional golf instruction content.
|
| 325 |
+
|
| 326 |
+
Instructions:
|
| 327 |
+
- Answer questions about golf swing technique, mechanics, common problems, and solutions
|
| 328 |
+
- Provide specific, actionable advice when possible
|
| 329 |
+
- Reference relevant technical concepts when appropriate
|
| 330 |
+
- Be encouraging and supportive
|
| 331 |
+
- Synthesize information from multiple sources rather than just quoting them
|
| 332 |
+
- Give clear, comprehensive explanations that golfers can understand and apply
|
| 333 |
+
|
| 334 |
+
Reference Materials from Golf Instruction Database:
|
| 335 |
+
{knowledge_context}"""
|
| 336 |
+
|
| 337 |
+
user_prompt = f"""Based on the golf instruction reference materials provided, please answer this question about golf swing technique:
|
| 338 |
+
|
| 339 |
+
{query}
|
| 340 |
+
|
| 341 |
+
Please provide a helpful, detailed response that synthesizes the relevant information into clear, actionable guidance."""
|
| 342 |
+
|
| 343 |
+
print(f"System prompt: {system_prompt}")
|
| 344 |
+
print(f"User prompt: {user_prompt}")
|
| 345 |
+
try:
|
| 346 |
+
response = rag_system.openai_client.chat.completions.create(
|
| 347 |
+
model="gpt-4o-mini",
|
| 348 |
+
messages=[
|
| 349 |
+
{"role": "system", "content": system_prompt},
|
| 350 |
+
{"role": "user", "content": user_prompt}
|
| 351 |
+
],
|
| 352 |
+
max_tokens=800,
|
| 353 |
+
temperature=0.7
|
| 354 |
+
)
|
| 355 |
+
return response.choices[0].message.content
|
| 356 |
+
except Exception as e:
|
| 357 |
+
print(f"OpenAI API error: {e}")
|
| 358 |
+
return generate_enhanced_fallback_response(query, context_chunks, user_swing_context)
|
| 359 |
+
|
| 360 |
+
def generate_enhanced_fallback_response(query, context_chunks, user_swing_context=""):
|
| 361 |
+
"""Generate an enhanced fallback response when OpenAI API is not available"""
|
| 362 |
+
if not context_chunks:
|
| 363 |
+
return "I couldn't find specific information about that topic in the golf swing database. Could you try rephrasing your question or being more specific?"
|
| 364 |
+
|
| 365 |
+
# Extract relevant information from chunks
|
| 366 |
+
best_chunk = context_chunks[0]
|
| 367 |
+
chunk_content = best_chunk['chunk']
|
| 368 |
+
source_title = best_chunk['metadata']['title']
|
| 369 |
+
|
| 370 |
+
response_parts = []
|
| 371 |
+
|
| 372 |
+
# Check if question is about swing motion biomechanics vs setup/grip/equipment
|
| 373 |
+
question_lower = query.lower()
|
| 374 |
+
|
| 375 |
+
# Define topics that are NOT about swing motion biomechanics
|
| 376 |
+
non_biomechanics_topics = [
|
| 377 |
+
'grip', 'hold', 'grip pressure', 'grip size', 'grip style',
|
| 378 |
+
'setup', 'stance', 'address', 'alignment', 'posture at address',
|
| 379 |
+
'equipment', 'club', 'ball', 'tee', 'glove',
|
| 380 |
+
'course management', 'strategy', 'mental', 'psychology',
|
| 381 |
+
'warm up', 'practice', 'routine', 'pre-shot'
|
| 382 |
+
]
|
| 383 |
+
|
| 384 |
+
# Check if question is about non-biomechanics topics
|
| 385 |
+
is_non_biomechanics = any(topic in question_lower for topic in non_biomechanics_topics)
|
| 386 |
+
|
| 387 |
+
# Part 1: Only check for relevant measurements if question is about swing motion biomechanics
|
| 388 |
+
found_relevant_measurement = False
|
| 389 |
+
if user_swing_context and not is_non_biomechanics:
|
| 390 |
+
analysis_content = user_swing_context.replace("USER'S SWING ANALYSIS:\n", "").strip()
|
| 391 |
+
analysis_lower = analysis_content.lower()
|
| 392 |
+
|
| 393 |
+
# Only do specific keyword matching for biomechanics-related questions
|
| 394 |
+
if "wrist" in question_lower and "hinge" in question_lower:
|
| 395 |
+
# Look for wrist hinge measurements (only if asking about wrist hinge specifically)
|
| 396 |
+
lines = analysis_content.split('\n')
|
| 397 |
+
for line in lines:
|
| 398 |
+
if 'wrist hinge' in line.lower() and ('°' in line or '%' in line):
|
| 399 |
+
import re
|
| 400 |
+
wrist_match = re.search(r'wrist hinge[:\s]*(\d+\.?\d*°)', line.lower())
|
| 401 |
+
if wrist_match:
|
| 402 |
+
response_parts.append(f"I notice that your wrist hinge is {wrist_match.group(1)} during your swing.")
|
| 403 |
+
found_relevant_measurement = True
|
| 404 |
+
break
|
| 405 |
+
|
| 406 |
+
elif "hip" in question_lower and ("rotation" in question_lower or "turn" in question_lower):
|
| 407 |
+
# Look for hip rotation measurements (only if asking about hip rotation/turn)
|
| 408 |
+
lines = analysis_content.split('\n')
|
| 409 |
+
for line in lines:
|
| 410 |
+
if 'hip rotation' in line.lower() and '°' in line:
|
| 411 |
+
import re
|
| 412 |
+
user_hip_match = re.search(r'-\s*hip rotation[:\s]*(\d+\.?\d*°)', line.lower())
|
| 413 |
+
if user_hip_match:
|
| 414 |
+
response_parts.append(f"I notice that your hip rotation is {user_hip_match.group(1)} during your swing.")
|
| 415 |
+
found_relevant_measurement = True
|
| 416 |
+
break
|
| 417 |
+
|
| 418 |
+
elif "weight" in question_lower and ("transfer" in question_lower or "shift" in question_lower):
|
| 419 |
+
# Look for weight transfer measurements (only if asking about weight transfer/shift)
|
| 420 |
+
lines = analysis_content.split('\n')
|
| 421 |
+
for line in lines:
|
| 422 |
+
if ('weight transfer' in line.lower() or 'weight shift' in line.lower()) and '%' in line:
|
| 423 |
+
import re
|
| 424 |
+
weight_match = re.search(r'weight (?:transfer|shift)[:\s]*(\d+\.?\d*%)', line.lower())
|
| 425 |
+
if weight_match:
|
| 426 |
+
response_parts.append(f"I notice that your weight transfer is {weight_match.group(1)} during the downswing.")
|
| 427 |
+
found_relevant_measurement = True
|
| 428 |
+
break
|
| 429 |
+
|
| 430 |
+
elif "shoulder" in question_lower and ("rotation" in question_lower or "turn" in question_lower):
|
| 431 |
+
# Look for shoulder measurements (only if asking about shoulder rotation/turn)
|
| 432 |
+
lines = analysis_content.split('\n')
|
| 433 |
+
for line in lines:
|
| 434 |
+
if 'shoulder rotation' in line.lower() and '°' in line:
|
| 435 |
+
import re
|
| 436 |
+
shoulder_match = re.search(r'shoulder rotation[:\s]*(\d+\.?\d*°)', line.lower())
|
| 437 |
+
if shoulder_match:
|
| 438 |
+
response_parts.append(f"I notice that your shoulder rotation is {shoulder_match.group(1)} during your swing.")
|
| 439 |
+
found_relevant_measurement = True
|
| 440 |
+
break
|
| 441 |
+
|
| 442 |
+
# Part 2: Expert recommendation (synthesized from source)
|
| 443 |
+
sentences = chunk_content.split('. ')
|
| 444 |
+
meaningful_sentences = [s.strip() for s in sentences if len(s.strip()) > 20][:3]
|
| 445 |
+
expert_advice = '. '.join(meaningful_sentences[:2]) + '.'
|
| 446 |
+
|
| 447 |
+
response_parts.append(f"Based on {source_title}, {expert_advice}")
|
| 448 |
+
|
| 449 |
+
# Part 3: Improvement recommendation (only connect to swing analysis if relevant)
|
| 450 |
+
if user_swing_context and found_relevant_measurement and not is_non_biomechanics:
|
| 451 |
+
# Only provide swing-analysis-specific advice if we found relevant measurements
|
| 452 |
+
analysis_content = user_swing_context.replace("USER'S SWING ANALYSIS:\n", "").strip()
|
| 453 |
+
response_parts.append("Based on your current measurements compared to professional standards, focus on implementing the expert advice above to address your specific swing characteristics.")
|
| 454 |
+
else:
|
| 455 |
+
# For non-biomechanics questions or when no relevant measurements found
|
| 456 |
+
response_parts.append("Focus on implementing this expert advice to improve your technique.")
|
| 457 |
+
|
| 458 |
+
# Combine all parts
|
| 459 |
+
final_response = "\n\n".join(response_parts)
|
| 460 |
+
|
| 461 |
+
# Add source reference
|
| 462 |
+
final_response += f"\n\n📚 **Source**: {source_title}"
|
| 463 |
+
|
| 464 |
+
return final_response
|
| 465 |
|
| 466 |
# Define functions
|
| 467 |
def validate_youtube_url(url):
|
|
|
|
| 535 |
'trajectory_data': None,
|
| 536 |
'sample_rate': None
|
| 537 |
}
|
| 538 |
+
if 'show_chatbot' not in st.session_state:
|
| 539 |
+
st.session_state.show_chatbot = False
|
| 540 |
+
|
| 541 |
# Add session cleanup - clean up old files when starting a new session
|
| 542 |
if 'session_initialized' not in st.session_state:
|
| 543 |
cleanup_result = cleanup_downloads_directory(keep_annotated=True)
|
|
|
|
| 668 |
'prompt': prompt
|
| 669 |
}
|
| 670 |
|
| 671 |
+
# Keep the original video file for potential annotation
|
| 672 |
+
# Video will be cleaned up when user uploads a new video or session ends
|
|
|
|
| 673 |
|
| 674 |
# Present the options after analysis
|
| 675 |
st.subheader("What would you like to do next?")
|
| 676 |
+
options_col1, options_col2, options_col3, options_col4 = st.columns(4)
|
| 677 |
|
| 678 |
with options_col1:
|
| 679 |
st.info(
|
|
|
|
| 689 |
st.info(
|
| 690 |
"**Option 3: Key Frame Analysis**\n\nExtract and review your setup, top of backswing, and impact frames with helpful comments for each phase."
|
| 691 |
)
|
| 692 |
+
|
| 693 |
+
with options_col4:
|
| 694 |
+
st.info(
|
| 695 |
+
"**Option 4: Golf Swing Chatbot**\n\nAsk specific questions about golf swing technique and get expert advice from our knowledge base."
|
| 696 |
+
)
|
| 697 |
|
| 698 |
except Exception as e:
|
| 699 |
st.error(f"Error during analysis: {str(e)}")
|
|
|
|
| 711 |
language="text")
|
| 712 |
|
| 713 |
# Create columns for the action buttons
|
| 714 |
+
button_col1, button_col2, button_col3, button_col4 = st.columns(4)
|
| 715 |
|
| 716 |
with button_col1:
|
| 717 |
annotated_video_clicked = st.button("Generate Annotated Video",
|
|
|
|
| 727 |
keyframe_analysis_clicked = st.button("Key Frame Analysis",
|
| 728 |
key="keyframe_analysis",
|
| 729 |
use_container_width=True)
|
| 730 |
+
|
| 731 |
+
with button_col4:
|
| 732 |
+
chatbot_clicked = st.button("Golf Swing Chatbot",
|
| 733 |
+
key="rag_chatbot",
|
| 734 |
+
use_container_width=True)
|
| 735 |
|
| 736 |
# Handle annotated video creation
|
| 737 |
if annotated_video_clicked:
|
| 738 |
+
# Reset chatbot state when other buttons are clicked
|
| 739 |
+
st.session_state.show_chatbot = False
|
| 740 |
try:
|
| 741 |
with st.spinner("Creating annotated video..."):
|
| 742 |
# Create downloads directory if it doesn't exist
|
|
|
|
| 783 |
|
| 784 |
# Handle improvement recommendations generation
|
| 785 |
if improvements_clicked:
|
| 786 |
+
# Reset chatbot state when other buttons are clicked
|
| 787 |
+
st.session_state.show_chatbot = False
|
| 788 |
with st.spinner(
|
| 789 |
"Analyzing your swing and generating recommendations..."):
|
| 790 |
# Get data from session state
|
|
|
|
| 827 |
else:
|
| 828 |
# Show error message if analysis failed
|
| 829 |
st.error(analysis)
|
| 830 |
+
|
| 831 |
# Handle key frame analysis (new tab/option)
|
| 832 |
if keyframe_analysis_clicked:
|
| 833 |
+
# Reset chatbot state when other buttons are clicked
|
| 834 |
+
st.session_state.show_chatbot = False
|
| 835 |
try:
|
| 836 |
with st.spinner("Extracting key frames from your swing..."):
|
| 837 |
user_video_path = st.session_state.analysis_data['video_path']
|
|
|
|
| 926 |
except Exception as e:
|
| 927 |
st.error(f"Error during key frame analysis: {str(e)}")
|
| 928 |
st.info("Please ensure your video is in a supported format and try again.")
|
| 929 |
+
|
| 930 |
+
# Handle RAG chatbot
|
| 931 |
+
if chatbot_clicked:
|
| 932 |
+
st.session_state.show_chatbot = True
|
| 933 |
+
|
| 934 |
+
# Always show chatbot interface if it's active
|
| 935 |
+
if st.session_state.show_chatbot:
|
| 936 |
+
# Create header with close button
|
| 937 |
+
header_col1, header_col2 = st.columns([3, 1])
|
| 938 |
+
with header_col1:
|
| 939 |
+
st.subheader("Golf Swing Technique Chatbot")
|
| 940 |
+
with header_col2:
|
| 941 |
+
if st.button("✕ Close Chatbot", use_container_width=True):
|
| 942 |
+
st.session_state.show_chatbot = False
|
| 943 |
+
st.rerun()
|
| 944 |
+
|
| 945 |
+
render_rag_interface()
|
| 946 |
|
| 947 |
|
| 948 |
if __name__ == "__main__":
|
app/utils/visualizer.py
CHANGED
|
@@ -216,9 +216,9 @@ def create_annotated_video(video_path,
|
|
| 216 |
print(f"Error transforming detection bbox: {str(e)}")
|
| 217 |
# Keep the bbox as is if there's an error
|
| 218 |
|
| 219 |
-
# Draw detections
|
| 220 |
frame_detections = [
|
| 221 |
-
d for d in detections if d.frame_idx == i * sample_rate
|
| 222 |
]
|
| 223 |
for detection in frame_detections:
|
| 224 |
try:
|
|
@@ -229,10 +229,8 @@ def create_annotated_video(video_path,
|
|
| 229 |
|
| 230 |
x1, y1, x2, y2 = map(int, detection.bbox)
|
| 231 |
|
| 232 |
-
# Draw bounding box
|
| 233 |
-
color = (0, 255,
|
| 234 |
-
0) if detection.class_name == "person" else (0, 0,
|
| 235 |
-
255)
|
| 236 |
cv2.rectangle(annotated_frame, (x1, y1), (x2, y2), color, 2)
|
| 237 |
|
| 238 |
# Draw label
|
|
|
|
| 216 |
print(f"Error transforming detection bbox: {str(e)}")
|
| 217 |
# Keep the bbox as is if there's an error
|
| 218 |
|
| 219 |
+
# Draw detections - only show person detections, skip other objects
|
| 220 |
frame_detections = [
|
| 221 |
+
d for d in detections if d.frame_idx == i * sample_rate and d.class_name == "person"
|
| 222 |
]
|
| 223 |
for detection in frame_detections:
|
| 224 |
try:
|
|
|
|
| 229 |
|
| 230 |
x1, y1, x2, y2 = map(int, detection.bbox)
|
| 231 |
|
| 232 |
+
# Draw bounding box (only for person detections - green)
|
| 233 |
+
color = (0, 255, 0) # Green for person
|
|
|
|
|
|
|
| 234 |
cv2.rectangle(annotated_frame, (x1, y1), (x2, y2), color, 2)
|
| 235 |
|
| 236 |
# Draw label
|
article_extractor.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
from newspaper import Article
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import time
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import re
|
| 7 |
+
from typing import List
|
| 8 |
+
|
| 9 |
+
def extract_article_text(urls):
|
| 10 |
+
"""Extract text content from a list of article URLs"""
|
| 11 |
+
articles = []
|
| 12 |
+
for url in urls:
|
| 13 |
+
try:
|
| 14 |
+
article = Article(url)
|
| 15 |
+
article.download()
|
| 16 |
+
article.parse()
|
| 17 |
+
|
| 18 |
+
articles.append({
|
| 19 |
+
'url': url,
|
| 20 |
+
'title': article.title,
|
| 21 |
+
'text': article.text,
|
| 22 |
+
'authors': article.authors,
|
| 23 |
+
'publish_date': article.publish_date,
|
| 24 |
+
'source': url.split('/')[2] # Extract domain
|
| 25 |
+
})
|
| 26 |
+
time.sleep(1) # Be respectful to servers
|
| 27 |
+
except Exception as e:
|
| 28 |
+
print(f"Failed to extract {url}: {e}")
|
| 29 |
+
|
| 30 |
+
return pd.DataFrame(articles)
|
| 31 |
+
|
| 32 |
+
def clean_text(text: str) -> str:
|
| 33 |
+
"""Clean text by removing extra whitespace and special characters"""
|
| 34 |
+
# Remove extra whitespace, special characters
|
| 35 |
+
text = re.sub(r'\s+', ' ', text)
|
| 36 |
+
text = re.sub(r'[^\w\s.,!?-]', '', text)
|
| 37 |
+
return text.strip()
|
| 38 |
+
|
| 39 |
+
def chunk_text(text: str, chunk_size: int = 1000, overlap: int = 200) -> List[str]:
|
| 40 |
+
"""Split text into overlapping chunks"""
|
| 41 |
+
words = text.split()
|
| 42 |
+
chunks = []
|
| 43 |
+
|
| 44 |
+
for i in range(0, len(words), chunk_size - overlap):
|
| 45 |
+
chunk = ' '.join(words[i:i + chunk_size])
|
| 46 |
+
chunks.append(chunk)
|
| 47 |
+
|
| 48 |
+
return chunks
|
| 49 |
+
|
| 50 |
+
def process_articles(urls: List[str], save_path: str = None) -> pd.DataFrame:
|
| 51 |
+
"""Complete pipeline to extract, clean, and process articles"""
|
| 52 |
+
print(f"Extracting text from {len(urls)} articles...")
|
| 53 |
+
|
| 54 |
+
# Extract articles
|
| 55 |
+
df = extract_article_text(urls)
|
| 56 |
+
|
| 57 |
+
# Clean text
|
| 58 |
+
df['cleaned_text'] = df['text'].apply(clean_text)
|
| 59 |
+
|
| 60 |
+
# Create chunks for each article
|
| 61 |
+
df['text_chunks'] = df['cleaned_text'].apply(
|
| 62 |
+
lambda x: chunk_text(x) if pd.notna(x) else []
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
# Save if path provided
|
| 66 |
+
if save_path:
|
| 67 |
+
df.to_csv(save_path, index=False)
|
| 68 |
+
print(f"Results saved to {save_path}")
|
| 69 |
+
|
| 70 |
+
return df
|
| 71 |
+
|
| 72 |
+
if __name__ == "__main__":
|
| 73 |
+
# Example usage
|
| 74 |
+
sample_urls = [
|
| 75 |
+
"https://example.com/article1",
|
| 76 |
+
"https://example.com/article2"
|
| 77 |
+
]
|
| 78 |
+
|
| 79 |
+
# Process articles
|
| 80 |
+
# df = process_articles(sample_urls, "extracted_articles.csv")
|
| 81 |
+
# print(f"Extracted {len(df)} articles")
|
| 82 |
+
|
| 83 |
+
print("Article extractor ready! Use process_articles() with your URLs.")
|
golf_swing_articles_complete.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements.txt
CHANGED
|
@@ -2,11 +2,20 @@ opencv-python-headless
|
|
| 2 |
yt-dlp==2025.05.22
|
| 3 |
ultralytics
|
| 4 |
mediapipe
|
| 5 |
-
numpy
|
| 6 |
-
matplotlib
|
| 7 |
torch==2.2.0
|
| 8 |
torchvision==0.17.0
|
| 9 |
-
openai==1.
|
| 10 |
python-dotenv==1.0.0
|
| 11 |
tqdm==4.66.1
|
| 12 |
-
streamlit==1.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
yt-dlp==2025.05.22
|
| 3 |
ultralytics
|
| 4 |
mediapipe
|
| 5 |
+
numpy==1.24.3
|
| 6 |
+
matplotlib==3.8.2
|
| 7 |
torch==2.2.0
|
| 8 |
torchvision==0.17.0
|
| 9 |
+
openai==1.12.0
|
| 10 |
python-dotenv==1.0.0
|
| 11 |
tqdm==4.66.1
|
| 12 |
+
streamlit==1.29.0
|
| 13 |
+
pandas==2.1.4
|
| 14 |
+
sentence-transformers==2.2.2
|
| 15 |
+
faiss-cpu==1.7.4
|
| 16 |
+
scikit-learn==1.3.2
|
| 17 |
+
plotly==5.17.0
|
| 18 |
+
langchain==0.1.7
|
| 19 |
+
langchain-openai==0.0.6
|
| 20 |
+
langchain-community==0.0.19
|
| 21 |
+
tiktoken==0.5.2
|