chenemii commited on
Commit
959d1ac
·
1 Parent(s): 0a3ecfc

Update Par-ity Project with enhanced features

Browse files
.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
- # Golf Swing Analysis
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
- - LLM-powered swing analysis and coaching tips (OpenAI GPT-4/3.5 or local Ollama models)
24
- - Annotated video generation
25
- - Key position comparison with professional golfer (3 critical swing positions)
26
- - Detailed improvement recommendations with visual analysis
27
-
28
- ## Setup
29
-
30
- 1. Clone the repository
31
- 2. Install the required packages:
32
- ```
33
- pip install -r requirements.txt
34
- ```
35
- 3. Set up the necessary directories:
36
- ```
37
- ./setup_directories.sh
38
- ```
39
- 4. Add a reference professional golfer video:
40
- - Save a video of a professional golfer's swing as `pro_golfer.mp4` in the `downloads` directory
41
- - This will be used for the side-by-side comparison feature
42
-
43
- 5. Set up LLM services for analysis (optional):
44
-
45
- **Option 1: OpenAI**
46
- - Set your OpenAI API key in `.streamlit/secrets.toml`:
47
- ```toml
48
- [openai]
49
- api_key = "your-openai-api-key-here"
50
- ```
51
-
52
- **Option 2: Ollama (Local LLM)**
53
- - Install and run Ollama locally: https://ollama.ai/
54
- - Configure in `.streamlit/secrets.toml`:
55
- ```toml
56
- [ollama]
57
- base_url = "http://localhost:11434/v1"
58
- model = "llama2" # or your preferred model
59
- ```
60
-
61
- **Option 3: Both Services**
62
- - Configure both in `.streamlit/secrets.toml` for automatic fallback
63
- - The app will try Ollama first, then OpenAI if Ollama fails
64
-
65
- **No Configuration**
66
- - The app works without any LLM configuration using sample analysis mode
67
-
68
- See `.streamlit/secrets.toml.example` for a complete configuration template.
69
-
70
- ## Running the Application
71
-
72
- Run the Streamlit app:
 
 
 
 
 
 
 
73
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  ./run_streamlit.sh
75
  ```
76
 
77
- Or manually:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  ```
79
- streamlit run app/streamlit_app.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  ```
81
 
82
- ## Usage
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- 1. Upload a golf swing video or provide a YouTube URL
 
 
 
85
  2. Click "Analyze Swing" to process the video
86
- 3. View the swing phase breakdown and metrics
87
- 4. Generate an annotated video showing the analysis
88
- 5. Compare your swing at 3 key positions with a professional golfer:
89
- - Starting position (setup)
90
- - Top of backswing
91
- - Impact with ball
92
- 6. Get detailed improvement recommendations for each swing phase
93
- 7. Download comparison images and analysis results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
- ## Technical Details
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- The application uses:
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**: 60-90° (Exceptional body turn and flexibility)
398
- - **Shoulder Rotation**: 120° (Full shoulder coil for maximum power)
399
- - **Posture Score**: 95-98% (Exceptional spine angle consistency)
400
- - **Weight Shift**: 70-88% (Excellent weight transfer to lead side)
401
 
402
  **Upper Body Excellence:**
403
- - **Arm Extension**: 96-100% (Near-perfect extension at impact)
404
- - **Wrist Hinge**: 95-120° (Optimal lag and release timing)
405
- - **Swing Plane Consistency**: 85% (Tour-level repeatability)
406
- - **Chest Rotation Efficiency**: 100% (Perfect coordination)
407
 
408
  **Power & Efficiency Markers:**
409
- - **Energy Transfer Efficiency**: 88-96% (Elite power transfer)
410
- - **Power Accumulation**: 100% (Maximum power generation)
411
- - **Sequential Kinematic Sequence**: 100% (Perfect body sequencing)
412
- - **Potential Distance**: 285-295 yards (Tour-level power)
413
 
414
  **Movement Quality Standards:**
415
- - **Head Movement**: 2-8 inches (Controlled, minimal excessive movement)
416
- - **Ground Force Efficiency**: 70-88% (Excellent ground interaction)
417
- - **Hip Thrust**: 40-100% (Strong lower body drive)
418
 
419
  ### **AMATEUR REFERENCE EXAMPLES FOR CALIBRATION:**
420
 
@@ -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 Varies Significantly**: From 23-90° in amateurs vs 60-90° in professionals
460
- 2. **Shoulder Rotation Range**: 60-120° in amateurs, professionals consistently at 120°
461
- 3. **Wrist Hinge Compensation**: Some amateurs (116.6°) exceed professional standards to compensate for limited body rotation
462
- 4. **Power Generation Methods**: Body-dominant amateurs can achieve near-professional distances despite technical limitations
463
- 5. **Head Movement Control**: Varies dramatically (3-8 inches) - major differentiator
464
- 6. **Energy Transfer Efficiency**: Ranges from 56.8-96.7% in amateurs vs 88-96% in professionals
465
- 7. **Weight Transfer Issues**: Some amateurs struggle with weight shift (50% vs professional 70-88%)
 
 
 
 
 
466
 
467
  ## CURRENT SWING ANALYSIS
468
 
@@ -523,125 +524,72 @@ Use these professional standards as your 100% reference for scoring. These repre
523
 
524
  ## ANALYSIS INSTRUCTIONS
525
 
526
- Using the professional benchmarks above as your calibration reference, provide your analysis in the following EXACT structured format:
 
527
 
528
- **PERFORMANCE_CLASSIFICATION:** [XX%] (where XX is a percentage from 10% to 100%)
 
529
 
530
  **STRENGTHS:**
531
- [Specific strength with direct comparison to professional benchmarks - e.g. "Your shoulder rotation shows great upper body mobility during your backswing, similar to what we see in professional swings"]
532
- • [Another strength with benchmark comparison - e.g. "Your arm extension at impact is really strong, demonstrating excellent fundamentals"]
533
- • [Third strength with specific metric comparison to benchmarks - e.g. "Your weight transfer demonstrates solid fundamentals in shifting from back foot to front foot through impact"]
 
 
534
 
535
- **WEAKNESSES:**
536
- [Specific area describing impact without numbers - e.g. "Your hip rotation is less than optimal, which may be limiting your power generation and overall swing efficiency"]
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
- **PRIORITY_IMPROVEMENTS:**
541
- 1. Topic Name - Explain what to focus on and when in swing, with encouraging tone and clear benefit explanation
542
- 2. Topic Name - What to work on with supportive guidance and positive reinforcement about potential improvements
543
- 3. Topic Name - Area to focus on with gentle direction and realistic goals
544
-
545
- **MANDATORY REQUIREMENTS FOR EACH SECTION:**
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
- **WEAKNESSES:**
582
- • Your hip rotation is less than optimal, which may be limiting your power generation and overall swing efficiency
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
- 1. Hip Mobility Development - Try increasing your hip rotation during the downswing. This will help you engage your lower body more effectively and unlock substantial power gains in your swing.
588
- 2. Head Stability Enhancement - Focus on keeping your head more stable throughout your swing. This improvement will help enhance your accuracy and consistency on every shot.
589
- 3. Wrist Hinge Optimization - Work on creating more wrist angle during your backswing to improve lag and power transfer. This will help add distance and control to your shots.
590
-
591
- PERFORMANCE CLASSIFICATION SCALE:
592
- - **90-100%**: Professional/Tour level - Consistently meets or exceeds professional benchmarks across all metrics
593
- - **80-89%**: Advanced amateur - Meets most professional standards with minor gaps in 1-2 areas
594
- - **70-79%**: Skilled amateur - Solid fundamentals with some gaps from professional standards
595
- - **60-69%**: Intermediate - Good basic mechanics but several areas need improvement to reach professional level
596
- - **50-59%**: Developing intermediate - Basic swing structure present but multiple areas below professional standards
597
- - **40-49%**: Advanced beginner - Some fundamentals in place but significant gaps in most areas
598
- - **30-39%**: Beginner - Basic swing motion present but major improvements needed across most metrics
599
- - **20-29%**: Novice - Limited swing fundamentals, extensive work needed on basic mechanics
600
- - **10-19%**: Complete beginner - Minimal swing structure, needs comprehensive fundamental development
601
-
602
- IMPORTANT ANALYSIS PRIORITIES (Based on Real Professional Data):
603
- 1. **PRIMARY FOCUS - Critical Biomechanical Differentiators**:
604
- - Hip Rotation (Professional: 60-90°, Amateur Range: 23-90°) - MOST IMPORTANT
605
- - Shoulder Rotation (Professional: 120°, Amateur Range: 60-120°) - MOST IMPORTANT
606
- - Sequential Kinematic Sequence (Professional: 100%, Amateur Range: 66.8-100%)
607
- - Energy Transfer Efficiency (Professional: 88-96%, Amateur Range: 56.8-96.7%)
608
-
609
- 2. **SECONDARY FOCUS - Power Generation Mechanics**:
610
- - Power Accumulation (Professional: 100%, Amateur Range: 82.1-100%)
611
- - Chest Rotation Efficiency (Professional: 100%, Amateur Range: 53.7-100%)
612
- - Wrist Hinge (Professional: 95-120°, Amateur Range: 49.4-116.6°)
613
- - Swing Plane Consistency (Professional: 85%, Amateur: 70-85%)
614
-
615
- 3. **TERTIARY FOCUS - Refinement Metrics**:
616
- - Posture Score (Professional: 95-98%, Amateur Range: 80.6-90.7%)
617
- - Arm Extension (Professional: 96-100%, Amateur Range: 94.8-100%)
618
- - Weight Shift (Professional: 70-88%, Amateur Range: 50-90%)
619
- - Ground Force Efficiency (Professional: 70-88%, Amateur Range: 50-90%)
620
-
621
- 4. **DE-EMPHASIZE - Timing Variables**: Frame counts, tempo ratios, and duration metrics vary significantly based on video capture rates and personal style preferences
622
-
623
- **SCORING CALIBRATION GUIDELINES:**
624
- - **Hip/Shoulder Rotation Analysis**: Compare to professional minimums (60° hip, 120° shoulder)
625
- - **Energy Transfer <70%**: Score below 60%, reference professional range (88-96%)
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
- # Extract numbered items with priority levels and descriptions
711
- priority_items = re.findall(r'(\d+)\.\s*\[(.*?)\]\s*(.*?)(?=\d+\.\s*\[|\Z)', priority_text, re.DOTALL)
712
- for num, priority_level, description in priority_items[:3]: # Limit to 3
713
- # Clean up the description
714
- description = description.strip()
715
- # Remove any trailing incomplete sentences
716
- if description.endswith('...') or len(description.split('.')[-1].strip()) < 5:
717
- sentences = description.split('.')
718
- if len(sentences) > 1:
719
- description = '.'.join(sentences[:-1]) + '.'
720
-
721
- formatted_analysis['priority_improvements'].append({
722
- 'rank': int(num),
723
- 'priority_level': priority_level.strip(),
724
- 'description': f"[{priority_level.strip()}] {description}"
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': '[Most Critical] Technical Refinement - Fine-tune specific mechanics to achieve consistency at the highest level.'},
798
- {'rank': 2, 'description': '[Important] Performance Optimization - Focus on maximizing efficiency and power transfer.'},
799
- {'rank': 3, 'description': '[Focus Area] Competitive Preparation - Enhance mental game and course management skills.'}
800
  ]
801
  elif percentage >= 60:
802
  formatted_analysis['priority_improvements'] = [
803
- {'rank': 1, 'description': '[Most Critical] Kinematic Sequence Enhancement - Improve body rotation coordination to generate more power and consistency.'},
804
- {'rank': 2, 'description': '[Important] Clubface Control - Enhance swing path consistency for better ball striking accuracy.'},
805
- {'rank': 3, 'description': '[Focus Area] Energy Transfer Efficiency - Optimize power transfer throughout the swing to maximize distance.'}
806
  ]
807
  elif percentage >= 40:
808
  formatted_analysis['priority_improvements'] = [
809
- {'rank': 1, 'description': '[Most Critical] Fundamental Mechanics - Establish consistent posture, grip, and setup positions.'},
810
- {'rank': 2, 'description': '[Important] Body Rotation Development - Improve hip and shoulder turn coordination.'},
811
- {'rank': 3, 'description': '[Focus Area] Weight Transfer - Develop proper weight shift from back foot to front foot during swing.'}
812
  ]
813
  else: # Below 40%
814
  formatted_analysis['priority_improvements'] = [
815
- {'rank': 1, 'description': '[Most Critical] Basic Setup and Posture - Focus on establishing proper spine angle and athletic stance.'},
816
- {'rank': 2, 'description': '[Important] Fundamental Swing Motion - Develop basic backswing and downswing mechanics.'},
817
- {'rank': 3, 'description': '[Focus Area] Balance and Stability - Improve overall balance throughout the swing motion.'}
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
- # Better extraction of improvement area and description
955
- area = ""
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
- area = parts[0].strip()
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
- # Try to extract first meaningful phrase as area
979
- words = description.split()
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) >= 33 and len(backswing_keypoints) >= 33:
1059
  # Hip rotation calculation using hip landmarks
1060
- setup_left_hip = np.array(setup_keypoints[23][:2])
1061
- setup_right_hip = np.array(setup_keypoints[24][:2])
1062
- backswing_left_hip = np.array(backswing_keypoints[23][:2])
1063
- backswing_right_hip = np.array(backswing_keypoints[24][:2])
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
- setup_hip_angle = np.degrees(np.arctan2(setup_hip_vector[1], setup_hip_vector[0]))
1070
- backswing_hip_angle = np.degrees(np.arctan2(backswing_hip_vector[1], backswing_hip_vector[0]))
1071
-
1072
- hip_rotation = abs(backswing_hip_angle - setup_hip_angle)
1073
- # Normalize to reasonable range (professionals typically achieve 45+ degrees)
1074
- metrics["hip_rotation"] = min(hip_rotation, 90)
1075
- else:
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) >= 33 and len(backswing_keypoints) >= 33:
1086
  # Shoulder rotation calculation
1087
- setup_left_shoulder = np.array(setup_keypoints[11][:2])
1088
- setup_right_shoulder = np.array(setup_keypoints[12][:2])
1089
- backswing_left_shoulder = np.array(backswing_keypoints[11][:2])
1090
- backswing_right_shoulder = np.array(backswing_keypoints[12][:2])
1091
 
1092
  setup_shoulder_vector = setup_right_shoulder - setup_left_shoulder
1093
  backswing_shoulder_vector = backswing_right_shoulder - backswing_left_shoulder
1094
 
1095
- setup_shoulder_angle = np.degrees(np.arctan2(setup_shoulder_vector[1], setup_shoulder_vector[0]))
1096
- backswing_shoulder_angle = np.degrees(np.arctan2(backswing_shoulder_vector[1], backswing_shoulder_vector[0]))
1097
-
1098
- shoulder_rotation = abs(backswing_shoulder_angle - setup_shoulder_angle)
1099
- metrics["shoulder_rotation"] = min(shoulder_rotation, 120)
1100
- else:
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) >= 33 and len(impact_keypoints) >= 33:
1111
  # Use center of mass approximation
1112
- setup_left_ankle = np.array(setup_keypoints[27][:2])
1113
- setup_right_ankle = np.array(setup_keypoints[28][:2])
1114
- impact_left_ankle = np.array(impact_keypoints[27][:2])
1115
- impact_right_ankle = np.array(impact_keypoints[28][:2])
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
- else:
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]) >= 33:
1141
  keypoints = pose_data[frame]
1142
  # Use shoulder and hip landmarks to estimate spine angle
1143
- left_shoulder = np.array(keypoints[11][:2])
1144
- right_shoulder = np.array(keypoints[12][:2])
1145
- left_hip = np.array(keypoints[23][:2])
1146
- right_hip = np.array(keypoints[24][:2])
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
- spine_angle = np.degrees(np.arctan2(spine_vector[1], spine_vector[0]))
1153
- posture_scores.append(abs(spine_angle))
 
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]) >= 33:
1164
  keypoints = pose_data[impact_frame]
1165
- right_shoulder = np.array(keypoints[12][:2])
1166
- right_elbow = np.array(keypoints[14][:2])
1167
- right_wrist = np.array(keypoints[16][:2])
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
- angles = calculate_joint_angles(pose_data[frame])
1192
- if "right_wrist" in angles:
1193
- wrist_angles.append(angles["right_wrist"])
 
 
 
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) >= 33 and len(impact_keypoints) >= 33:
1208
  # Use nose landmark (index 0) for head position
1209
- setup_head = np.array(setup_keypoints[0][:2])
1210
- impact_head = np.array(impact_keypoints[0][:2])
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
- head_height_pixels = abs(setup_keypoints[0][1] - setup_keypoints[10][1]) # Nose to mouth
 
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]) >= 33:
1243
  keypoints = pose_data[frame]
1244
  # Right knee angle using hip, knee, ankle
1245
- right_hip = np.array(keypoints[24][:2])
1246
- right_knee = np.array(keypoints[26][:2])
1247
- right_ankle = np.array(keypoints[28][:2])
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
- # Fail here
1327
- return None
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
- # Clean up the original video file after processing (keep frames in memory)
241
- st.info("🗑️ Cleaning up original video file to save space...")
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(3)
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(3)
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.6.0
10
  python-dotenv==1.0.0
11
  tqdm==4.66.1
12
- streamlit==1.30.0
 
 
 
 
 
 
 
 
 
 
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