""" Streamlit web UI for Golf Swing Analysis """ import os import sys import tempfile import streamlit as st from dotenv import load_dotenv import base64 from pathlib import Path import shutil import cv2 from PIL import Image from datetime import datetime # Load environment variables load_dotenv() # Add the app directory to the path sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # ===== FORCE MODULE RELOAD FOR UPDATED METRICS ===== # This ensures the latest front_facing_metrics.py fixes are loaded import importlib modules_to_reload = [ 'models.front_facing_metrics', 'models.metrics_calculator', 'models.pose_estimator', 'models.swing_analyzer', 'models.llm_analyzer' ] for module in modules_to_reload: if module in sys.modules: importlib.reload(sys.modules[module]) print(f"π Reloaded {module}") # Enable debug mode for front-facing metrics try: import models.front_facing_metrics as ffm ffm.set_debug(True) print(f"β Front-facing metrics version: {ffm.METRICS_VERSION}") print(f"β Debug enabled: {ffm.VERBOSE}") except Exception as e: print(f"β οΈ Could not enable debug mode: {e}") # Clear Streamlit caches to ensure fresh data if hasattr(st, 'cache_data'): st.cache_data.clear() if hasattr(st, 'cache_resource'): st.cache_resource.clear() print("π Module reloads complete - running with latest fixes!") # ===== END MODULE RELOAD SECTION ===== # Import modules (will use reloaded versions from above) from utils.video_downloader import download_youtube_video, download_pro_reference, cleanup_video_file, cleanup_downloads_directory from utils.video_processor import process_video from models.pose_estimator import analyze_pose from models.swing_analyzer import segment_swing_pose_based, analyze_trajectory from models.llm_analyzer import generate_swing_analysis, create_llm_prompt, prepare_data_for_llm, check_llm_services, parse_and_format_analysis, display_formatted_analysis, compute_core_metrics from utils.visualizer import create_annotated_video from utils.comparison import create_key_frame_comparison, extract_key_swing_frames # Import RAG functionality print("=== RAG Import Debug Information ===") print(f"Current working directory: {os.getcwd()}") print(f"Python path: {sys.path}") print(f"Files in current directory: {os.listdir('.')}") # Check if we're in the app directory or project root if os.path.exists("golf_swing_rag.py"): print("β Found golf_swing_rag.py in current directory") elif os.path.exists("app/golf_swing_rag.py"): print("β Found golf_swing_rag.py in app/ subdirectory") else: print("β golf_swing_rag.py not found in current directory or app/ subdirectory") print(f"Looking for: golf_swing_rag.py") if os.path.exists("app"): print(f"Files in app directory: {os.listdir('app')}") try: print("Attempting to import golf_swing_rag...") from golf_swing_rag import GolfSwingRAG print("β Successfully imported GolfSwingRAG from golf_swing_rag") RAG_AVAILABLE = True except ImportError as e: print(f"β ImportError: {e}") print("Trying alternative import methods...") # Try importing from app directory explicitly try: print("Trying: from app.golf_swing_rag import GolfSwingRAG") from app.golf_swing_rag import GolfSwingRAG print("β Successfully imported from app.golf_swing_rag") RAG_AVAILABLE = True except ImportError as e2: print(f"β App import failed: {e2}") # Try adding current directory to path and importing try: print("Adding current directory to sys.path and trying again...") current_dir = os.path.dirname(os.path.abspath(__file__)) if current_dir not in sys.path: sys.path.insert(0, current_dir) print(f"Added to path: {current_dir}") from golf_swing_rag import GolfSwingRAG print("β Successfully imported after adding current dir to path") RAG_AVAILABLE = True except ImportError as e3: print(f"β Final import attempt failed: {e3}") RAG_AVAILABLE = False st.error(f"RAG functionality not available. Import errors: {e}, {e2}, {e3}") if RAG_AVAILABLE: print("β RAG system is available!") else: print("β RAG system is NOT available") st.warning("RAG functionality not available. Please ensure golf_swing_rag.py is in the app directory.") print("=== End RAG Import Debug ===") print("") # Set page config st.set_page_config(page_title="Par-ity ProjectποΈββοΈ", page_icon="ποΈββοΈ", layout="wide", initial_sidebar_state="collapsed") # Custom CSS for RAG interface st.markdown(""" """, unsafe_allow_html=True) @st.cache_resource def load_rag_system(): """Load and initialize the RAG system (cached for performance) with enhanced error handling""" if not RAG_AVAILABLE: st.warning("RAG system not available - missing dependencies") return None try: print("=== RAG System Loading Debug ===") with st.spinner("Loading golf swing knowledge base..."): print("Creating GolfSwingRAG instance...") rag = GolfSwingRAG() print("β GolfSwingRAG instance created successfully") print("Loading and processing data...") rag.load_and_process_data() print("β Data loaded and processed successfully") print("Creating embeddings (this may take a moment)...") try: rag.create_embeddings() if hasattr(rag, 'index') and rag.index is not None: print("β Embeddings created successfully with FAISS") st.success("π― RAG system loaded with semantic search capabilities") else: print("β Embeddings creation had issues, but fallback search available") st.warning("β οΈ RAG system loaded with basic search (semantic search unavailable)") except Exception as embedding_error: print(f"β Embedding creation failed: {embedding_error}") print("RAG will use fallback search methods") st.warning("β οΈ RAG system loaded with basic search only") print("β RAG system initialization completed!") print("=== End RAG System Loading Debug ===") return rag except Exception as e: print(f"β Critical error loading RAG system: {str(e)}") print(f"Error type: {type(e).__name__}") import traceback print(f"Full traceback: {traceback.format_exc()}") st.error(f"β RAG system failed to load: {str(e)}") return None def clamp_for_display(value, min_val, max_val): """Clamp a value for display purposes only""" if value == 'n/a' or value is None: return value try: float_val = float(value) return max(min_val, min(max_val, float_val)) except (ValueError, TypeError): return value def format_metric_value(metric_data, unit=""): """Format metric value with status indication""" if not isinstance(metric_data, dict): return 'n/a' value = metric_data.get('value') status = metric_data.get('status', 'n/a') if value is None or status == 'n/a': return 'n/a' elif status == 'ok': return f"{value}{unit}" else: return f"{value} ({status}){unit}" def get_back_tilt_grading(value, confidence, camera_roll=0): """Grade back tilt at setup""" if value is None: return {'value': None, 'display_value': 'n/a', 'badge': 'βͺ', 'status': 'No data available'} # Note: Not hiding based on camera roll - display all measurements # Determine grading if 28 <= value <= 38: badge = "π’" label = "On-plane posture" elif (24 <= value < 28) or (38 < value <= 42): badge = "π " label = "Slightly out of range" elif (20 <= value < 24) or (42 < value <= 48): badge = "π " label = "Off (adjust)" else: badge = "π΄" label = "Likely problematic" return { 'value': value, # Raw value for display logic 'display_value': f"{value:.1f}Β°", 'badge': badge, 'label': label, 'confidence': confidence, 'status': label, # Use label as status for legacy compatibility } def get_knee_flexion_grading(value, confidence): """Grade knee flexion at setup""" if value is None: return {'display_value': 'n/a', 'badge': 'βͺ', 'status': 'No data available'} # Determine grading if 15 <= value <= 30: badge = "π’" label = "Athletic" elif (12 <= value < 15): badge = "π " label = "Slightly too stiff" elif (30 < value <= 35): badge = "π " label = "Slightly too bent" elif (8 <= value < 12): badge = "π " label = "Too stiff" elif (35 < value <= 40): badge = "π " label = "Too bent" else: badge = "π΄" label = "Likely problematic" return { 'display_value': f"{value:.1f}Β°", 'badge': badge, 'label': label, 'confidence': confidence } def get_shoulder_tilt_swing_plane_grading(value, confidence): """Grade shoulder tilt/swing plane at top - professional = 36Β°, 30 handicap = 29Β°""" if value is None: return {'value': None, 'display_value': 'n/a', 'badge': 'βͺ', 'status': 'No data available'} # Professional = 36Β°, 30 handicap = 29Β° if 30 <= value <= 40: badge = 'π’' label = 'Excellent plane' elif 25 <= value < 30 or 40 < value <= 45: badge = 'π ' label = 'Good plane' else: badge = 'π΄' label = 'Needs adjustment' return { 'value': value, # Raw value for display logic 'display_value': f'{value:.1f}Β°', 'badge': badge, 'label': label, 'confidence': confidence, 'status': label # Use label as status for legacy compatibility } def get_head_drop_grading(value, confidence): """Grade head movement at top based on percentage of torso length Convention: positive = moved DOWN (drop), negative = moved UP (rise) Grades by absolute movement magnitude, shows direction in display. """ if value is None: return {'value': None, 'display_value': 'n/a', 'badge': 'βͺ', 'status': 'No data available'} # Convert string to float if needed try: if isinstance(value, str): # Remove any non-numeric characters except decimal point and minus sign import re clean_value = re.sub(r'[^\d.-]', '', value) if clean_value: value = float(clean_value) else: return {'value': None, 'display_value': 'n/a', 'badge': 'βͺ', 'status': 'Invalid value'} else: value = float(value) except (ValueError, TypeError): return {'value': None, 'display_value': 'n/a', 'badge': 'βͺ', 'status': 'Invalid value'} # Determine direction and absolute magnitude direction = "drop" if value >= 0 else "rise" abs_movement = abs(value) # Grade by absolute movement magnitude (suggested rubric) if abs_movement <= 3: badge = 'π’' grade = 'Excellent head stability' elif abs_movement <= 6: badge = 'π ' grade = 'Good/typical' elif abs_movement <= 10: badge = 'β οΈ' grade = 'Borderline (work on stability)' else: # abs_movement > 10 badge = 'π΄' grade = 'Excessive movement' return { 'value': value, # Raw value for display logic 'display_value': f'{abs_movement:.1f}% {direction}', 'badge': badge, 'label': grade, 'confidence': confidence, 'status': grade # Use grade as status for legacy compatibility } def get_torso_sidebend_impact_grading(value, confidence): """Grade torso side-bend at impact - professional range ~10-20Β° (trail-side bend positive)""" if value is None: return {'display_value': 'n/a', 'badge': 'βͺ', 'status': 'No data available'} # Use absolute value for grading, but keep sign for display abs_value = abs(value) # Professional range ~10-20Β°, typical amateur ~6-24Β° if 10 <= abs_value <= 20: badge = 'π’' label = 'Excellent side-bend' elif 6 <= abs_value < 10 or 20 < abs_value <= 24: badge = 'π ' label = 'Good side-bend' else: badge = 'π΄' label = 'Needs work' return { 'display_value': f'{value:.1f}Β°', 'badge': badge, 'label': label, 'confidence': confidence } def get_hip_shoulder_separation_impact_grading(value, confidence): """Grade hip-shoulder separation at impact - typical range 10-45 degrees""" if value is None: return {'display_value': 'n/a', 'badge': 'βͺ', 'status': 'No data available'} # Professional golfers typically show 20-35Β° separation, recreational 10-25Β° if 20 <= value <= 35: badge = 'π’' label = 'Excellent separation' elif 15 <= value < 20 or 35 < value <= 45: badge = 'π ' label = 'Good separation' else: badge = 'π΄' label = 'Needs improvement' return { 'display_value': f'{value:.1f}Β°', 'badge': badge, 'label': label, 'confidence': confidence } def get_hip_sway_grading(value, confidence, position="top"): """Grade hip sway - professional = 3.9" towards target, 30 handicap = 2.5" towards target""" if value is None: return {'display_value': 'n/a', 'badge': 'βͺ', 'status': 'No data available'} # Professional = 3.9" towards target, 30 handicap = 2.5" towards target if position == "top": if 3.0 <= value <= 5.0: badge = 'π’' label = 'Excellent sway' elif 2.0 <= value < 3.0 or 5.0 < value <= 6.0: badge = 'π ' label = 'Good sway' else: badge = 'π΄' label = 'Needs improvement' else: # impact if 2.0 <= value <= 4.0: badge = 'π’' label = 'Excellent sway' elif 1.0 <= value < 2.0 or 4.0 < value <= 5.0: badge = 'π ' label = 'Good sway' else: badge = 'π΄' label = 'Needs improvement' return { 'display_value': f'{value:.1f}"', 'badge': badge, 'label': label, 'confidence': confidence } def get_wrist_hinge_grading(value, confidence, position="top"): """Grade wrist hinge angle""" if value is None: return {'display_value': 'n/a', 'badge': 'βͺ', 'status': 'No data available'} # Professional ranges: Top ~90-120Β°, Impact ~15-35Β° if position == "top": if 85 <= value <= 125: badge = 'π’' label = 'Excellent hinge' elif 70 <= value < 85 or 125 < value <= 140: badge = 'π ' label = 'Good hinge' else: badge = 'π΄' label = 'Needs improvement' else: # impact if 15 <= value <= 40: badge = 'π’' label = 'Excellent release' elif 10 <= value < 15 or 40 < value <= 50: badge = 'π ' label = 'Good release' else: badge = 'π΄' label = 'Needs improvement' return { 'display_value': f"{value:.1f}Β°", 'badge': badge, 'label': label, 'confidence': confidence, } def display_new_grading_scheme(core_metrics): """Display the swing analysis with badges and confidence indicators""" # Check if we have front-facing metrics # Front-facing metrics have unique keys that DTL doesn't have has_front_facing_metrics = any(key in core_metrics for key in [ 'torso_side_bend_deg', 'shoulder_tilt_impact_deg', 'hip_sway_top_inches', 'wrist_hinge_top_deg', 'hip_shoulder_separation_impact_deg' ]) if has_front_facing_metrics: st.subheader("Swing Analysis") else: st.subheader("Down-the-Line Swing Analysis") # Extract raw values and calculate confidence (simplified for now) # New DTL metrics shoulder_tilt_swing_plane_data = core_metrics.get("shoulder_tilt_swing_plane_top_deg", {}) back_tilt_data = core_metrics.get("back_tilt_deg", {}) knee_flexion_data = core_metrics.get("knee_flexion_deg", {}) head_drop_data = core_metrics.get("head_drop_top_pct", {}) hip_depth_data = core_metrics.get("hip_depth_early_extension", {}) # Calculate confidence with QC penalties as per feedback def get_confidence(data, metric_type='general'): if data.get('value') is None: return 0.0 # Start with base confidence of 90% confidence = 90.0 status = data.get('status', 'n/a') # Apply QC penalties as specified in feedback # conf = base (90) β occlusion(0β25) β scale/crop change(0β10) β phase underseg(0β15) β sign unknown(0β5) # Occlusion penalties (0-25 points) if 'club_not_visible' in status or 'Unavailable (club occluded)' in status: confidence -= 25 # Maximum occlusion penalty elif 'poor tracking' in status or 'no detection' in status: confidence -= 20 elif 'approximate' in status: confidence -= 15 elif 'low confidence' in status: confidence -= 10 # Scale/crop change penalties (0-10 points) if 'scale_drift' in status or 'unstable (scale)' in status: confidence -= 10 elif 'QC fail' in status: confidence -= 8 # Phase undersegmentation penalties (0-15 points) if 'timing_unreliable' in status or 'phase underseg' in status: confidence -= 15 elif 'unreliable' in status: confidence -= 10 # Sign unknown/uncertain penalties (0-5 points) if 'uncertain' in status or 'extreme value' in status: confidence -= 5 elif 'outside tour range' in status: confidence -= 3 # Metric-specific adjustments if metric_type == 'shaft_angle': if 'target_line_error' in status: return 0.0 # Complete failure elif 'club occluded' in status: confidence = 0.0 # Never show if club not visible elif metric_type == 'head_sway': if 'tracking_failed' in status: return 0.0 # Complete failure elif metric_type == 'wrist_pattern': # Check for club visibility issues if 'insufficient_data' in status: confidence -= 20 # Cap at 75% if club tip visibility uncertain confidence = min(confidence, 75.0) elif metric_type == 'hip_rotation': # DTL-only limitation - cap at 60% confidence = min(confidence, 60.0) elif metric_type == 'shoulder_turn': # DTL-only limitation - cap at 65% confidence = min(confidence, 65.0) # Apply DTL-only caps: confidence β€70% by default for DTL-limited metrics if 'DTL' in status or 'approx' in status: confidence = min(confidence, 70.0) # If primitive is n/a, dependent metrics β€75% if data.get('value') is None: confidence = min(confidence, 75.0) # Ensure confidence stays within valid range confidence = max(0.0, min(90.0, confidence)) return confidence / 100.0 # Convert to 0-1 scale # Process each metric metrics_to_display = [] # Old metrics removed - now using new 5-metric system # DTL Metrics - All 5 new metrics if not has_front_facing_metrics: # 1. Shoulder Tilt/Swing Plane @ Top shoulder_tilt_swing_plane_value = shoulder_tilt_swing_plane_data.get('value') if shoulder_tilt_swing_plane_value is not None: confidence = get_confidence(shoulder_tilt_swing_plane_data, 'shoulder_tilt_swing_plane') grading = get_shoulder_tilt_swing_plane_grading(shoulder_tilt_swing_plane_value, confidence) if grading: metrics_to_display.append(("Shoulder Tilt/Swing Plane @ Top", grading)) # 3. Back Tilt @ Setup tilt_value = back_tilt_data.get('value') if tilt_value is not None: tilt_confidence = get_confidence(back_tilt_data, 'back_tilt') tilt_grading = get_back_tilt_grading(tilt_value, tilt_confidence) if tilt_grading: metrics_to_display.append(("Back Tilt @ Setup", tilt_grading)) # 4. Knee Flexion @ Setup knee_value = knee_flexion_data.get('value') if knee_value is not None: # Apply knee flexion correction if needed (handle legacy data) if knee_value > 90: knee_value = 180.0 - knee_value knee_confidence = get_confidence(knee_flexion_data, 'knee_flexion') knee_grading = get_knee_flexion_grading(knee_value, knee_confidence) if knee_grading: metrics_to_display.append(("Knee Flexion", knee_grading)) # 5. Head Movement @ Top (New DTL metric) head_drop_value = head_drop_data.get('value') if head_drop_value is not None: head_drop_confidence = get_confidence(head_drop_data, 'head_drop') head_drop_grading = get_head_drop_grading(head_drop_value, head_drop_confidence) if head_drop_grading: metrics_to_display.append(("Head Movement @ Top", head_drop_grading)) # Removed: Wrist Pattern, Kinematic Sequence (DTL-approx), and Shoulder Turn Quality metrics # 8. Hip Depth / Early Extension (New DTL metric) hip_depth_value = hip_depth_data.get('value') if hip_depth_value is not None: hip_confidence = get_confidence(hip_depth_data, 'hip_depth') # Pass the detailed_data (full dictionary) instead of just the value detailed_data = hip_depth_data.get('detailed_data', {}) hip_grading = get_hip_depth_grading(detailed_data, hip_confidence) if hip_grading: metrics_to_display.append(("Hip Depth / Early Extension", hip_grading)) else: # Debug: Check why hip grading is None st.caption(f"Debug: Hip depth value: {hip_depth_value}, confidence: {hip_confidence}") elif hip_depth_data.get('error'): # Display error message error_data = hip_depth_data.get('detailed_data', {}) error_msg = error_data.get('error', 'Unknown error') error_grading = { 'display_value': f'Error: {error_msg}', 'badge': 'π΄', 'label': 'Calculation failed', 'confidence': 0.0, } metrics_to_display.append(("Hip Depth / Early Extension", error_grading)) else: pass # Hip depth calculation succeeded but no special handling needed # Additional old metrics removed - focusing on new 4-metric system # Front-facing metrics (only displayed when available) - 4 required metrics if has_front_facing_metrics: # Front-facing metrics are now always calculated pass # Torso Side-Bend at Impact (replaces shoulder tilt) torso_sidebend_data = core_metrics.get("torso_side_bend_deg", {}) if torso_sidebend_data.get('value') is not None: confidence = 0.9 # High confidence for front-facing measurements grading = get_torso_sidebend_impact_grading(torso_sidebend_data['value'], confidence) if grading: metrics_to_display.append(("Torso Side-Bend @ Impact", grading)) else: # Fallback to old metric name for compatibility shoulder_tilt_impact_data = core_metrics.get("shoulder_tilt_impact_deg", {}) if shoulder_tilt_impact_data.get('value') is not None: confidence = 0.9 # High confidence for front-facing measurements grading = get_torso_sidebend_impact_grading(shoulder_tilt_impact_data['value'], confidence) if grading: metrics_to_display.append(("Torso Side-Bend @ Impact", grading)) # Hip-Shoulder Separation at Impact hip_shoulder_sep_data = core_metrics.get("hip_shoulder_separation_impact_deg", {}) if hip_shoulder_sep_data.get('value') is not None: confidence = hip_shoulder_sep_data.get('confidence', 0.8) grading = get_hip_shoulder_separation_impact_grading(hip_shoulder_sep_data['value'], confidence) if grading: metrics_to_display.append(("Hip-Shoulder Separation @ Impact", grading)) # Hip Sway at Top hip_sway_top_data = core_metrics.get("hip_sway_top_inches", {}) if hip_sway_top_data.get('value') is not None: confidence = 0.8 grading = get_hip_sway_grading(hip_sway_top_data['value'], confidence, "top") if grading: metrics_to_display.append(("Hip Sway @ Top", grading)) # Wrist Hinge at Top wrist_hinge_top_data = core_metrics.get("wrist_hinge_top_deg", {}) if wrist_hinge_top_data.get('value') is not None: confidence = 0.8 # Lower confidence as it's estimated from pose grading = get_wrist_hinge_grading(wrist_hinge_top_data['value'], confidence, "top") if grading: metrics_to_display.append(("Wrist Hinge @ Top", grading)) # Display each metric for metric_name, grading in metrics_to_display: display_metric_card(metric_name, grading) def display_metric_card(metric_name, grading): """Display a single metric as a clean text bubble using Streamlit components""" # Create a container for each metric with st.container(): # Use Streamlit's built-in styling st.markdown("---") # Metric header st.subheader(metric_name) # Add definition definition = get_metric_definition(metric_name) if definition: st.caption(definition) # Result line with badge and label result_line = f"**{grading['display_value']}** β {grading['badge']} {grading.get('label', '')}" st.markdown(result_line) # Confidence display removed per user request # Detailed evaluation text evaluation = get_metric_evaluation(metric_name, grading) st.write(evaluation) # Tips display removed per user request # Add spacing st.write("") def get_metric_definition(metric_name): """Get a short definition for each metric""" definitions = { "Shoulder Tilt/Swing Plane @ Top": "Measures shoulder swing plane angle at the top of backswing.", "Back Tilt @ Setup": "Measures spine angle from vertical at address position.", "Knee Flexion": "Measures knee bend angle at address position.", "Head Movement @ Top": "Measures head movement during backswing as percentage of torso length.", "Hip Depth / Early Extension": "Tracks loss of hip flexion through impact.", "Torso Side-Bend @ Impact": "Measures torso side-bend angle at ball contact.", "Shoulder Tilt @ Impact": "Measures shoulder angle at ball contact.", # Deprecated "Hip-Shoulder Separation @ Impact": "Measures hip rotation relative to shoulders at ball contact.", "Hip Sway @ Top": "Measures lateral hip movement at the top of backswing.", "Wrist Hinge @ Top": "Measures wrist hinge angle at the top of backswing." } return definitions.get(metric_name, "") def get_metric_evaluation(metric_name, grading): """Generate detailed evaluation text for each metric""" value = grading.get('display_value', 'Unknown') badge = grading.get('badge', '') # DTL METRICS (5 current metrics) if metric_name == "Shoulder Tilt/Swing Plane @ Top": if "π’" in badge: return f"Your shoulder tilt/swing plane of **{value}** at the top shows excellent position. This optimal swing plane angle promotes powerful, on-plane delivery and consistent ball striking. Professional golfers typically maintain 36Β° while 30-handicappers average 29Β°. Your measurement indicates proper shoulder turn and swing plane control." elif "π " in badge: return f"Your shoulder tilt/swing plane of **{value}** at the top is good with room for improvement. This measurement affects your swing plane consistency and power generation. Refining your shoulder turn and spine angle can enhance ball striking and distance control." else: return f"Your shoulder tilt/swing plane of **{value}** at the top needs attention. This metric is crucial for swing plane consistency and power generation. Work on proper shoulder rotation and maintaining spine angle throughout the backswing for better results." elif metric_name == "Back Tilt @ Setup": if "π’" in badge: return f"Your back tilt of **{value}** shows excellent posture setup. This forward spine angle is crucial for creating the proper swing plane and generating power through impact. Good back tilt promotes consistent contact, optimal launch conditions, and prevents early extension during the downswing." elif "π " in badge: return f"Your back tilt of **{value}** is acceptable but could be optimized. Back tilt affects your swing plane and ability to rotate properly. Slight adjustments to your setup posture could improve consistency, distance, and ball striking quality." else: return f"Your back tilt of **{value}** needs attention for optimal performance. Proper spine angle at setup is fundamental for swing mechanics, power generation, and consistent ball contact. Poor back tilt can lead to swing compensations and inconsistent results." elif metric_name == "Knee Flexion": if "π’" in badge: return f"Your knee flexion of **{value}** demonstrates an athletic setup position. This optimal knee bend provides stability throughout the swing while allowing proper weight transfer and rotation. Good knee flexion supports powerful, balanced swings and consistent ball striking." elif "π " in badge: return f"Your knee flexion of **{value}** is workable but could be refined. Knee bend affects your balance, power transfer, and ability to maintain posture during the swing. Minor adjustments could enhance your stability and swing efficiency." else: return f"Your knee flexion of **{value}** may be limiting your swing potential. Proper knee bend is essential for balance, power generation, and maintaining spine angle. Too little or too much knee flexion can cause balance issues and inconsistent contact." elif metric_name == "Head Movement @ Top": if "π’" in badge: return f"Your head drop of **{value}** shows excellent head stability during the backswing. This minimal movement indicates proper head control and balance, which promotes consistent contact and accuracy. Good head stability is fundamental for reliable ball striking." elif "π " in badge: return f"Your head drop of **{value}** shows moderate movement during the backswing. Some head movement can affect balance and consistency. Working on head stability and maintaining your spine angle can improve ball striking and accuracy." else: return f"Your head drop of **{value}** indicates excessive head movement during the backswing. Too much head drop disrupts balance and swing center, leading to inconsistent contact. Focus on keeping your head stable and maintaining spine angle for better results." elif metric_name == "Hip Depth / Early Extension": if "π’" in badge: return f"Your hip depth of **{value}** shows excellent posture maintenance. This indicates you're maintaining proper spine angle and avoiding early extension through impact. Good hip depth promotes solid contact, power transfer, and consistent ball striking patterns." elif "π " in badge: return f"Your hip depth of **{value}** is acceptable but could be improved. This measurement indicates some early extension tendencies. Working on maintaining spine angle and hip position through impact can enhance consistency and power transfer." else: return f"Your hip depth of **{value}** indicates early extension issues. This movement pattern reduces power transfer and can cause inconsistent contact. Focus on maintaining spine angle and proper hip position throughout the downswing and impact." # FRONT-FACING METRICS (4 current metrics) elif metric_name == "Torso Side-Bend @ Impact": # Interpret the sign side_description = "trail-side" if float(value.replace('Β°', '')) > 0 else "lead-side" abs_value = abs(float(value.replace('Β°', ''))) if "π’" in badge: return f"Your torso side-bend of **{value}** at impact shows excellent position. You're bending **{abs_value:.1f}Β°** toward the **{side_description}** which indicates ideal impact dynamics and power transfer. This optimal torso angle promotes solid contact, optimal ball flight, and consistent distance control." elif "π " in badge: return f"Your torso side-bend of **{value}** at impact is acceptable with room for improvement. You're bending **{abs_value:.1f}Β°** toward the **{side_description}**. Torso position at impact affects power transfer and ball flight characteristics. Refining your impact position can enhance consistency and distance." else: return f"Your torso side-bend of **{value}** at impact needs attention. You're bending **{abs_value:.1f}Β°** toward the **{side_description}**. Proper torso angle at impact is crucial for power transfer and ball flight control. Work on impact position for better contact and consistency." elif metric_name == "Shoulder Tilt @ Impact": # Legacy support if "π’" in badge: return f"Your shoulder tilt of **{value}** at impact shows excellent position. This proper shoulder angle indicates ideal impact dynamics and power transfer. Good shoulder tilt at impact promotes solid contact, optimal ball flight, and consistent distance control." elif "π " in badge: return f"Your shoulder tilt of **{value}** at impact is acceptable with room for improvement. Shoulder position at impact affects power transfer and ball flight characteristics. Refining your impact position can enhance consistency and distance." else: return f"Your shoulder tilt of **{value}** at impact needs attention. Proper shoulder angle at impact is crucial for power transfer and ball flight control. Work on impact position for better contact and consistency." elif metric_name == "Hip-Shoulder Separation @ Impact": if "π’" in badge: return f"Your hip-shoulder separation of **{value}** at impact shows excellent body sequencing. This proper rotation sequence indicates ideal power transfer with the hips leading the shoulders through impact. Good separation promotes solid contact and optimal ball flight." elif "π " in badge: return f"Your hip-shoulder separation of **{value}** at impact is acceptable with room for improvement. Hip-shoulder sequencing affects power transfer and swing efficiency. Refining your rotation sequence can enhance consistency and distance." else: return f"Your hip-shoulder separation of **{value}** at impact needs attention. Proper sequencing with hips leading shoulders is crucial for power transfer and ball flight control. Work on rotation timing for better contact and consistency." elif metric_name == "Hip Sway @ Top": if "π’" in badge: return f"Your hip sway of **{value}** at the top shows excellent stability. Minimal lateral movement maintains proper balance and swing center, promoting consistent contact and accuracy. This stable foundation supports powerful, controlled swings." elif "π " in badge: return f"Your hip sway of **{value}** at the top shows moderate movement. Some lateral sway can affect balance and consistency. Working on stability and weight transfer can improve ball striking and accuracy." else: return f"Your hip sway of **{value}** at the top indicates excessive lateral movement. Too much sway disrupts balance and swing center, leading to inconsistent contact. Focus on stability and proper weight transfer for better results." elif metric_name == "Wrist Hinge @ Top": if "π’" in badge: return f"Your wrist hinge of **{value}** at the top shows excellent set. This proper wrist angle stores energy effectively and sets up lag for powerful release through impact. Good wrist hinge contributes significantly to clubhead speed and distance." elif "π " in badge: return f"Your wrist hinge of **{value}** at the top is adequate but could be optimized. Better wrist action can enhance lag, power generation, and strike consistency. Work on wrist mobility and proper hinge timing." else: return f"Your wrist hinge of **{value}** at the top needs improvement. Proper wrist set is crucial for creating lag and power. Limited wrist hinge reduces potential clubhead speed and distance. Focus on wrist mobility and hinge mechanics." else: # Default evaluation for any other metrics return f"Your **{value}** measurement provides insight into your swing mechanics. This metric affects various aspects of your performance including power generation, accuracy, and consistency. Continue working on this fundamental for improved golf performance." def display_swing_phase_breakdown(swing_phases): """Display the swing phase breakdown table""" st.subheader("Swing Phase Breakdown") # Create phase data phase_data = [] for phase_name, phase_info in swing_phases.items(): phase_data.append([ phase_name.title().replace('_', ' '), phase_info.get('frame_count', 0), f"{phase_info.get('duration_ms', 0):.0f} ms" ]) # Display as table import pandas as pd df = pd.DataFrame(phase_data, columns=["Phase", "Frames", "Duration"]) # Style the table styled_df = df.style.set_properties(**{ 'background-color': '#f8f9fa', 'color': '#0B3B0B', 'border': '1px solid #dee2e6' }).set_table_styles([ {'selector': 'th', 'props': [('background-color', '#e9ecef'), ('color', '#0B3B0B'), ('font-weight', 'bold')]}, {'selector': 'td', 'props': [('text-align', 'center')]}, {'selector': 'th:first-child', 'props': [('text-align', 'left')]}, {'selector': 'td:first-child', 'props': [('text-align', 'left'), ('font-weight', 'bold')]} ]) st.dataframe(styled_df, use_container_width=True, hide_index=True) def display_rag_sources(sources): """Display source information in an organized way""" if not sources: return st.subheader("π Sources") for i, source in enumerate(sources[:3]): # Show top 3 sources with st.expander(f"Source {i+1}: {source['metadata']['title'][:60]}..."): st.write(f"**Similarity Score:** {source['similarity_score']:.3f}") st.write(f"**Source:** {source['metadata']['source']}") if source['metadata']['url']: st.write(f"**URL:** [Link]({source['metadata']['url']})") st.write("**Content:**") st.write(source['chunk'][:500] + "..." if len(source['chunk']) > 500 else source['chunk']) def render_rag_interface(): """Render the RAG chatbot interface""" # Removed header and description # Initialize RAG system if 'rag_system' not in st.session_state and RAG_AVAILABLE: st.session_state.rag_system = load_rag_system() # Initialize chat history if not exists if 'rag_chat_history' not in st.session_state: st.session_state.rag_chat_history = [] if not RAG_AVAILABLE or st.session_state.get('rag_system') is None: st.error("RAG system is not available. Please check the setup.") return # Check if we have video analysis data to enhance responses user_swing_context = "" if st.session_state.get('video_analyzed') and 'analysis_data' in st.session_state: stored_data = st.session_state.analysis_data # Use the structured analysis_data instead of just the prompt if 'analysis_data' in stored_data: structured_analysis = stored_data['analysis_data'] core_metrics = structured_analysis.get('core_metrics', {}) # Format the simplified data for better RAG context user_swing_context = f""" USER'S SWING ANALYSIS: === SWING TIMING & PHASES === Swing Phases: - Setup: {structured_analysis.get('swing_phases', {}).get('setup', {}).get('frame_count', 0)} frames - Backswing: {structured_analysis.get('swing_phases', {}).get('backswing', {}).get('frame_count', 0)} frames - Downswing: {structured_analysis.get('swing_phases', {}).get('downswing', {}).get('frame_count', 0)} frames - Impact: {structured_analysis.get('swing_phases', {}).get('impact', {}).get('frame_count', 0)} frames - Follow-through: {structured_analysis.get('swing_phases', {}).get('follow_through', {}).get('frame_count', 0)} frames Timing Metrics: - Total Swing Time: {structured_analysis.get('timing_metrics', {}).get('total_swing_time_ms', 'N/A')} ms === DTL METRICS === - Shoulder Tilt/Swing Plane @ Top: {format_metric_value(core_metrics.get('shoulder_tilt_swing_plane_top_deg', {}), 'Β°')} - Back Tilt @ Setup: {format_metric_value(core_metrics.get('back_tilt_deg', {}), 'Β°')} - Knee Flexion @ Setup: {format_metric_value(core_metrics.get('knee_flexion_deg', {}), 'Β°')} - Head Movement @ Top: {format_metric_value(core_metrics.get('head_drop_top_pct', {}), '%')} - Hip Depth / Early Extension: {format_metric_value(core_metrics.get('hip_depth_early_extension', {}), '%')} === DTL-LIMITED METRICS (Approximate) === - Shoulder Turn Quality: {format_metric_value(core_metrics.get('shoulder_turn_quality', {}))} === FRONT-FACING METRICS === - Torso Side-Bend @ Impact: {format_metric_value(core_metrics.get('torso_side_bend_deg', {}), 'Β°')} - Hip-Shoulder Separation @ Impact: {format_metric_value(core_metrics.get('hip_shoulder_separation_impact_deg', {}), 'Β°')} - Hip Sway @ Top: {format_metric_value(core_metrics.get('hip_sway_top_inches', {}), '"')} - Wrist Hinge @ Top: {format_metric_value(core_metrics.get('wrist_hinge_top_deg', {}), 'Β°')} """ # Removed success message elif 'prompt' in stored_data: # Fallback to prompt if structured data not available user_swing_context = f"\n\nUSER'S SWING ANALYSIS:\n{stored_data['prompt']}" # Removed success message # Create columns for layout col1, col2 = st.columns([2, 1]) with col1: # Removed subheader # Question input (with proper label) question = st.text_area( "Question", # Proper label for accessibility height=100, placeholder="Ask about your golf swing technique...", label_visibility="collapsed" # Hide the label visually while keeping it for accessibility ) # Removed settings section - using smart defaults instead col_submit, col_clear = st.columns([1, 1]) with col_submit: submit_button = st.button("π― Get Answer", type="primary", use_container_width=True) with col_clear: if st.button("ποΈ Clear Chat History", use_container_width=True): st.session_state.rag_chat_history = [] # Don't call st.rerun() here to avoid disappearing interface st.success("Chat history cleared!") # Process question if submit_button and question.strip(): with st.spinner("Analyzing your question and searching the knowledge base..."): try: # Enhanced query method that includes user's swing context # Use smart default for number of sources (3-5 depending on context) num_sources = 5 if user_swing_context else 3 # More sources when we have swing analysis result = query_with_user_context( st.session_state.rag_system, question, user_swing_context, top_k=num_sources ) # Add to chat history st.session_state.rag_chat_history.append({ 'question': question, 'response': result['response'], 'sources': result['sources'], 'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 'used_swing_context': bool(user_swing_context) }) st.success("Answer generated successfully!") except Exception as e: st.error(f"An error occurred: {str(e)}") # Display chat history (simplified) if st.session_state.rag_chat_history: for i, chat in enumerate(reversed(st.session_state.rag_chat_history)): # Removed question numbers, timestamps, and personalization indicators # Question st.markdown(f'
', unsafe_allow_html=True) # Response st.markdown(f'', unsafe_allow_html=True) # Removed sources display st.divider() with col2: # Removed all the About section, Tips, Personalized Questions, and metrics pass def query_with_user_context(rag_system, question, user_swing_context, top_k=5): """Enhanced query method that includes user's swing analysis context""" # Search for relevant chunks relevant_chunks = rag_system.search_similar_chunks(question, top_k) # Generate response with enhanced context response = generate_enhanced_response(rag_system, question, relevant_chunks, user_swing_context) print(f"Response: {response}") return { 'response': response, 'sources': relevant_chunks, 'query': question, 'timestamp': datetime.now().isoformat() } def generate_enhanced_response(rag_system, query, context_chunks, user_swing_context=""): """Generate response using OpenAI API with user's swing analysis as the main system prompt""" if not rag_system.openai_client: print("No OpenAI client found") return generate_enhanced_fallback_response(query, context_chunks, user_swing_context) # Prepare context from knowledge base knowledge_context = "\n\n".join([f"Reference Material from '{chunk['metadata']['title']}':\n{chunk['chunk']}" for chunk in context_chunks]) # Use the user's swing analysis as the primary system prompt if available print(f"User swing context: {user_swing_context}") if user_swing_context: # Extract the actual analysis content (remove the header) analysis_content = user_swing_context.replace("USER'S SWING ANALYSIS:\n", "").strip() system_prompt = f"""{analysis_content} You are a golf swing technique expert assistant analyzing this specific player's swing. INSTRUCTIONS: 1. Always answer golf technique questions using the reference materials below 2. For swing motion biomechanics questions (head movement, hip rotation, weight transfer, etc.), also reference specific measurements from the player's swing analysis above when relevant 3. For setup/stance questions, answer from the reference materials without needing to reference swing motion data 4. Provide clear, actionable advice based on proven golf instruction 5. IMPORTANT: Keep responses to 4 sentences or less - be concise and focused Reference Materials from Golf Instruction Database: {knowledge_context}""" user_prompt = f"""Based on the golf instruction reference materials provided, please answer this question about golf swing technique: {query} Please provide a helpful, concise response (4 sentences or less) that addresses the specific question while drawing from the relevant information in the context. If the question relates to swing motion biomechanics and you have specific measurements from my swing analysis above, include those details for personalized advice.""" else: # Fallback to general system prompt if no swing analysis available 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. Instructions: - Answer questions about golf swing technique, mechanics, common problems, and solutions - Provide specific, actionable advice when possible - Reference relevant technical concepts when appropriate - Be encouraging and supportive - Synthesize information from multiple sources rather than just quoting them - IMPORTANT: Keep responses to 4 sentences or less - be concise and focused Reference Materials from Golf Instruction Database: {knowledge_context}""" user_prompt = f"""Based on the golf instruction reference materials provided, please answer this question about golf swing technique: {query} Please provide a helpful, concise response (4 sentences or less) that synthesizes the relevant information into clear, actionable guidance.""" print(f"System prompt: {system_prompt}") print(f"User prompt: {user_prompt}") try: response = rag_system.openai_client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], max_tokens=400, temperature=0.7 ) return response.choices[0].message.content except Exception as e: print(f"OpenAI API error: {e}") return generate_enhanced_fallback_response(query, context_chunks, user_swing_context) def generate_enhanced_fallback_response(query, context_chunks, user_swing_context=""): """Generate an enhanced fallback response when OpenAI API is not available""" if not context_chunks: 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?" # Extract relevant information from chunks best_chunk = context_chunks[0] chunk_content = best_chunk['chunk'] source_title = best_chunk['metadata']['title'] response_parts = [] # Check if question is about swing motion biomechanics vs setup/grip/equipment question_lower = query.lower() # Define topics that are NOT about swing motion biomechanics non_biomechanics_topics = [ 'grip', 'hold', 'grip pressure', 'grip size', 'grip style', 'setup', 'stance', 'address', 'alignment', 'posture at address', 'equipment', 'club', 'ball', 'tee', 'glove', 'course management', 'strategy', 'mental', 'psychology', 'warm up', 'practice', 'routine', 'pre-shot' ] # Check if question is about non-biomechanics topics is_non_biomechanics = any(topic in question_lower for topic in non_biomechanics_topics) # Part 1: Only check for relevant measurements if question is about swing motion biomechanics found_relevant_measurement = False if user_swing_context and not is_non_biomechanics: analysis_content = user_swing_context.replace("USER'S SWING ANALYSIS:\n", "").strip() analysis_lower = analysis_content.lower() # Only do specific keyword matching for biomechanics-related questions if "wrist" in question_lower and "hinge" in question_lower: # Look for wrist hinge measurements (only if asking about wrist hinge specifically) lines = analysis_content.split('\n') for line in lines: if 'wrist hinge' in line.lower() and ('Β°' in line or '%' in line): import re wrist_match = re.search(r'wrist hinge[:\s]*(\d+\.?\d*Β°)', line.lower()) if wrist_match: response_parts.append(f"I notice that your wrist hinge is {wrist_match.group(1)} during your swing.") found_relevant_measurement = True break elif "head" in question_lower and ("movement" in question_lower or "moving" in question_lower or "steady" in question_lower): # Look for head movement measurements (only if asking about head movement) lines = analysis_content.split('\n') for line in lines: if 'head movement' in line.lower() and ('in' in line or 'inches' in line): import re lateral_match = re.search(r'head movement \(lateral\)[:\s]*(\d+\.?\d*)\s*in', line.lower()) vertical_match = re.search(r'head movement \(vertical\)[:\s]*(\d+\.?\d*)\s*in', line.lower()) if lateral_match or vertical_match: lateral_val = lateral_match.group(1) if lateral_match else "N/A" vertical_val = vertical_match.group(1) if vertical_match else "N/A" response_parts.append(f"I notice that your head movement is {lateral_val} inches laterally and {vertical_val} inches vertically during your swing.") found_relevant_measurement = True break # Hip rotation question handling removed per user request elif "weight" in question_lower and ("transfer" in question_lower or "shift" in question_lower): # Look for weight transfer measurements (only if asking about weight transfer/shift) lines = analysis_content.split('\n') for line in lines: if ('weight transfer' in line.lower() or 'weight shift' in line.lower()) and '%' in line: import re weight_match = re.search(r'weight (?:transfer|shift)[:\s]*(\d+\.?\d*%)', line.lower()) if weight_match: response_parts.append(f"I notice that your weight transfer is {weight_match.group(1)} during the downswing.") found_relevant_measurement = True break elif "shoulder" in question_lower and ("rotation" in question_lower or "turn" in question_lower): # Look for shoulder measurements (only if asking about shoulder rotation/turn) lines = analysis_content.split('\n') for line in lines: if 'shoulder rotation' in line.lower() and 'Β°' in line: import re shoulder_match = re.search(r'shoulder rotation[:\s]*(\d+\.?\d*Β°)', line.lower()) if shoulder_match: response_parts.append(f"I notice that your shoulder rotation is {shoulder_match.group(1)} during your swing.") found_relevant_measurement = True break # Part 2: Expert recommendation (synthesized from source - keep concise) sentences = chunk_content.split('. ') meaningful_sentences = [s.strip() for s in sentences if len(s.strip()) > 20][:2] expert_advice = '. '.join(meaningful_sentences[:2]) + '.' response_parts.append(f"Based on {source_title}, {expert_advice}") # Part 3: Improvement recommendation (only connect to swing analysis if relevant) if user_swing_context and found_relevant_measurement and not is_non_biomechanics: # Only provide swing-analysis-specific advice if we found relevant measurements response_parts.append("Focus on implementing this expert advice to address your specific swing characteristics.") else: # For non-biomechanics questions or when no relevant measurements found response_parts.append("Focus on implementing this expert advice.") # Combine all parts with space separation to keep it concise final_response = " ".join(response_parts) return final_response # Define functions def validate_youtube_url(url): """Validate if the URL is a YouTube URL""" return "youtube.com" in url or "youtu.be" in url def process_uploaded_video(uploaded_file): """Process an uploaded video file""" # Create downloads directory if it doesn't exist os.makedirs("downloads", exist_ok=True) # Save uploaded file to the downloads directory file_path = os.path.join("downloads", uploaded_file.name) with open(file_path, "wb") as f: f.write(uploaded_file.getvalue()) return file_path def display_video(video_path, width=300): """Display a video with download option""" # Read video bytes with open(video_path, "rb") as file: video_bytes = file.read() # Create a container with custom width video_container = st.container() # Apply CSS to control the width and ensure it's centered video_container.markdown(f""" """, unsafe_allow_html=True) # Display video using st.video with bytes with video_container: st.video(video_bytes) # Show download button st.download_button(label="Download Video", data=video_bytes, file_name=os.path.basename(video_path), mime="video/mp4") # Main app def main(): """Main Streamlit application with 5-step flow""" # Custom CSS for Par-ity branding st.markdown(""" """, unsafe_allow_html=True) # Logo at the top try: # Try multiple possible paths for the logo file logo_paths = [ "3in par-ity project horizontal logo.png", # New 3-inch logo "par-ity project horizontal logo.png", # Fallback to original "app/3in par-ity project horizontal logo.png", # Original path for local development "app/par-ity project horizontal logo.png", # Fallback original path "./3in par-ity project horizontal logo.png" # Explicit current directory ] logo_loaded = False for logo_path in logo_paths: if os.path.exists(logo_path): col1, col2, col3 = st.columns([1, 2, 1]) with col2: st.image(logo_path) logo_loaded = True break if not logo_loaded: st.markdown('Get personalized swing analysis with specific tips for improvement
Ask specific questions about golf swing technique from our knowledge base
{overall_summary}