from flask import Flask, request, jsonify, send_file from flask_cors import CORS import pandas as pd import os import tempfile import json from athletic_performance import ( analyze_youtube_video, analyze_video_file, get_performance_insights, get_ai_sports_coaching_analysis, test_gemini_api_connection, generate_annotated_video_from_youtube, generate_annotated_video_from_file ) app = Flask(__name__) CORS(app) # Enable CORS for all routes @app.route('/health', methods=['GET']) def health_check(): """Health check endpoint""" return jsonify({"status": "healthy", "message": "Athletic Performance API is running"}) @app.route('/analyze/youtube', methods=['POST']) def analyze_youtube(): """Analyze jump from YouTube URL""" try: data = request.get_json() # Validate required fields required_fields = ['youtube_url', 'user_height_cm', 'user_weight_kg'] for field in required_fields: if field not in data: return jsonify({"error": f"Missing required field: {field}"}), 400 youtube_url = data['youtube_url'] user_height_cm = data['user_height_cm'] user_weight_kg = data['user_weight_kg'] # Validate input values if not youtube_url or not youtube_url.strip(): return jsonify({"error": "YouTube URL cannot be empty"}), 400 if user_height_cm <= 0 or user_weight_kg <= 0: return jsonify({"error": "Height and weight must be positive values"}), 400 # Create a simple progress callback (no-op for API) def progress_callback(prog, desc): pass # Call the core analysis function result = analyze_youtube_video(youtube_url, user_height_cm, user_weight_kg, progress_callback) # Handle errors if "error" in result: return jsonify({"error": result['error']}), 400 if result is None: return jsonify({"error": "Could not analyze jump. Make sure the video shows a person clearly performing a vertical jump."}), 400 # Get performance insights insights = get_performance_insights(result) # Format response response = { "success": True, "analysis": { "jump_height_cm": result.get('jump_height_cm', 0), "flight_time_s": result.get('flight_time_s', 0), "normalized_rise": result.get('normalized_rise', 0), "peak_power_watts": result.get('peak_power_watts', 0), "peak_force_n": result.get('peak_force_n', 0), "impulse_ns": result.get('impulse_ns', 0), "rate_of_force_development": result.get('rate_of_force_development', 0), "takeoff_phase_duration_s": result.get('takeoff_phase_duration_s', 0), "ground_contact_time_s": result.get('ground_contact_time_s', 0), "frames": result.get('frames', 0), "fps": result.get('fps', 0), "video": result.get('video', ''), "user_weight_kg": result.get('user_weight_kg', user_weight_kg) }, "insights": insights, "relative_jump_height": (result.get('jump_height_cm', 0) / user_height_cm * 100) if user_height_cm > 0 else 0 } return jsonify(response) except Exception as e: return jsonify({"error": f"Unexpected error: {str(e)}"}), 500 @app.route('/analyze/file', methods=['POST']) def analyze_file(): """Analyze jump from uploaded video file""" try: # Check if file is uploaded if 'video_file' not in request.files: return jsonify({"error": "No video file uploaded"}), 400 file = request.files['video_file'] if file.filename == '': return jsonify({"error": "No file selected"}), 400 # Get form data user_height_cm = request.form.get('user_height_cm', type=float) user_weight_kg = request.form.get('user_weight_kg', type=float) # Validate required fields if not user_height_cm or not user_weight_kg: return jsonify({"error": "Missing required fields: user_height_cm and user_weight_kg"}), 400 if user_height_cm <= 0 or user_weight_kg <= 0: return jsonify({"error": "Height and weight must be positive values"}), 400 # Save uploaded file temporarily with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file: file.save(temp_file.name) temp_file_path = temp_file.name try: # Create a simple progress callback (no-op for API) def progress_callback(prog, desc): pass # Call the core analysis function result = analyze_video_file(temp_file_path, user_height_cm, user_weight_kg, progress_callback) # Handle errors if "error" in result: return jsonify({"error": result['error']}), 400 if result is None: return jsonify({"error": "Could not analyze jump. Make sure the video shows a person clearly performing a vertical jump."}), 400 # Get performance insights insights = get_performance_insights(result) # Format response response = { "success": True, "analysis": { "jump_height_cm": result.get('jump_height_cm', 0), "flight_time_s": result.get('flight_time_s', 0), "normalized_rise": result.get('normalized_rise', 0), "peak_power_watts": result.get('peak_power_watts', 0), "peak_force_n": result.get('peak_force_n', 0), "impulse_ns": result.get('impulse_ns', 0), "rate_of_force_development": result.get('rate_of_force_development', 0), "takeoff_phase_duration_s": result.get('takeoff_phase_duration_s', 0), "ground_contact_time_s": result.get('ground_contact_time_s', 0), "frames": result.get('frames', 0), "fps": result.get('fps', 0), "video": result.get('video', ''), "user_weight_kg": result.get('user_weight_kg', user_weight_kg) }, "insights": insights, "relative_jump_height": (result.get('jump_height_cm', 0) / user_height_cm * 100) if user_height_cm > 0 else 0 } return jsonify(response) finally: # Clean up temporary file if os.path.exists(temp_file_path): os.unlink(temp_file_path) except Exception as e: return jsonify({"error": f"Unexpected error: {str(e)}"}), 500 @app.route('/ai-coaching', methods=['POST']) def ai_coaching(): """Get AI-powered sports coaching recommendations""" try: data = request.get_json() # Validate required fields required_fields = ['user_height_cm', 'user_weight_kg', 'gender', 'gemini_api_key', 'favorite_sports'] for field in required_fields: if field not in data: return jsonify({"error": f"Missing required field: {field}"}), 400 user_height_cm = data['user_height_cm'] user_weight_kg = data['user_weight_kg'] gender = data['gender'] gemini_api_key = data['gemini_api_key'] favorite_sports = data['favorite_sports'] # Validate inputs if not gemini_api_key or not gemini_api_key.strip(): return jsonify({"error": "Gemini API key is required"}), 400 if not gender: return jsonify({"error": "Gender is required"}), 400 if user_height_cm <= 0 or user_weight_kg <= 0: return jsonify({"error": "Height and weight must be positive values"}), 400 # Validate favorite_sports if not favorite_sports or not isinstance(favorite_sports, list): return jsonify({"error": "favorite_sports must be a non-empty list"}), 400 if len(favorite_sports) > 5: return jsonify({"error": "Maximum 5 favorite sports allowed"}), 400 # Clean and validate sport names cleaned_sports = [] for sport in favorite_sports: if isinstance(sport, str) and sport.strip(): cleaned_sports.append(sport.strip()) if not cleaned_sports: return jsonify({"error": "Please provide valid sport names"}), 400 # Determine video source youtube_url = data.get('youtube_url', '') video_file_data = data.get('video_file_data', '') # Base64 encoded video data if not youtube_url and not video_file_data: return jsonify({"error": "Either YouTube URL or video file data is required"}), 400 # First, get the jump analysis def progress_callback(prog, desc): pass if youtube_url: result = analyze_youtube_video(youtube_url, user_height_cm, user_weight_kg, progress_callback) else: # Handle base64 video data import base64 video_data = base64.b64decode(video_file_data) with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file: temp_file.write(video_data) temp_file_path = temp_file.name try: result = analyze_video_file(temp_file_path, user_height_cm, user_weight_kg, progress_callback) finally: if os.path.exists(temp_file_path): os.unlink(temp_file_path) # Handle analysis errors if "error" in result: return jsonify({"error": f"Video analysis failed: {result['error']}"}), 400 if result is None: return jsonify({"error": "Could not analyze jump. Please ensure the video shows a clear vertical jump."}), 400 # Get AI coaching analysis ai_result = get_ai_sports_coaching_analysis( jump_height_cm=result['jump_height_cm'], user_height_cm=user_height_cm, gender=gender, favorite_sports=cleaned_sports, peak_power_watts=result.get('peak_power_watts'), flight_time_s=result.get('flight_time_s'), rfd=result.get('rate_of_force_development'), api_key=gemini_api_key.strip() ) if "error" in ai_result: return jsonify({"error": f"AI analysis failed: {ai_result['error']}"}), 400 # Format response response = { "success": True, "performance_summary": { "jump_height_cm": result.get('jump_height_cm', 0), "relative_jump_height": (result.get('jump_height_cm', 0) / user_height_cm * 100) if user_height_cm > 0 else 0, "flight_time_s": result.get('flight_time_s', 0), "peak_power_watts": result.get('peak_power_watts', 0), "gender": gender, "height_cm": user_height_cm, "weight_kg": user_weight_kg }, "ai_coaching_analysis": ai_result['analysis'] } return jsonify(response) except Exception as e: return jsonify({"error": f"Unexpected error: {str(e)}"}), 500 @app.route('/test-api-key', methods=['POST']) def test_api_key(): """Test Gemini API key connection""" try: data = request.get_json() if 'api_key' not in data: return jsonify({"error": "API key is required"}), 400 api_key = data['api_key'] if not api_key or not api_key.strip(): return jsonify({"error": "API key cannot be empty"}), 400 result = test_gemini_api_connection(api_key.strip()) response = { "success": result["success"], "status_code": result["status_code"], "response_text": result["response_text"], "error": result.get("error", None) } return jsonify(response) except Exception as e: return jsonify({"error": f"Unexpected error: {str(e)}"}), 500 @app.route('/generate-video/youtube', methods=['POST']) def generate_video_youtube(): """Generate annotated video from YouTube URL""" try: data = request.get_json() # Validate required fields required_fields = ['youtube_url', 'user_height_cm', 'user_weight_kg', 'gender'] for field in required_fields: if field not in data: return jsonify({"error": f"Missing required field: {field}"}), 400 youtube_url = data['youtube_url'] user_height_cm = data['user_height_cm'] user_weight_kg = data['user_weight_kg'] gender = data['gender'] # Validate inputs if not youtube_url or not youtube_url.strip(): return jsonify({"error": "YouTube URL cannot be empty"}), 400 if not gender: return jsonify({"error": "Gender is required"}), 400 if user_height_cm <= 0 or user_weight_kg <= 0: return jsonify({"error": "Height and weight must be positive values"}), 400 # Create progress callback def progress_callback(prog, desc): pass # Call the video generation function result = generate_annotated_video_from_youtube( youtube_url, user_height_cm, user_weight_kg, gender, progress_callback ) # Handle errors if "error" in result: return jsonify({"error": f"Video generation failed: {result['error']}"}), 400 if result is None: return jsonify({"error": "Could not generate video. Please ensure the video shows a clear vertical jump."}), 400 # Check if video file exists video_path = result.get("output_video_path", "") if not video_path or not os.path.exists(video_path): return jsonify({"error": "Generated video file not found"}), 500 # Return video file return send_file( video_path, as_attachment=True, download_name=f"annotated_jump_analysis_{user_height_cm}cm_{user_weight_kg}kg.mp4", mimetype='video/mp4' ) except Exception as e: return jsonify({"error": f"Unexpected error: {str(e)}"}), 500 @app.route('/generate-video/file', methods=['POST']) def generate_video_file(): """Generate annotated video from uploaded file""" try: # Check if file is uploaded if 'video_file' not in request.files: return jsonify({"error": "No video file uploaded"}), 400 file = request.files['video_file'] if file.filename == '': return jsonify({"error": "No file selected"}), 400 # Get form data user_height_cm = request.form.get('user_height_cm', type=float) user_weight_kg = request.form.get('user_weight_kg', type=float) gender = request.form.get('gender') # Validate required fields if not user_height_cm or not user_weight_kg or not gender: return jsonify({"error": "Missing required fields: user_height_cm, user_weight_kg, and gender"}), 400 if user_height_cm <= 0 or user_weight_kg <= 0: return jsonify({"error": "Height and weight must be positive values"}), 400 # Save uploaded file temporarily with tempfile.NamedTemporaryFile(delete=False, suffix='.mp4') as temp_file: file.save(temp_file.name) temp_file_path = temp_file.name try: # Create progress callback def progress_callback(prog, desc): pass # Call the video generation function result = generate_annotated_video_from_file( temp_file_path, user_height_cm, user_weight_kg, gender, progress_callback ) # Handle errors if "error" in result: return jsonify({"error": f"Video generation failed: {result['error']}"}), 400 if result is None: return jsonify({"error": "Could not generate video. Please ensure the video shows a clear vertical jump."}), 400 # Check if video file exists video_path = result.get("output_video_path", "") if not video_path or not os.path.exists(video_path): return jsonify({"error": "Generated video file not found"}), 500 # Return video file return send_file( video_path, as_attachment=True, download_name=f"annotated_jump_analysis_{user_height_cm}cm_{user_weight_kg}kg.mp4", mimetype='video/mp4' ) finally: # Clean up temporary file if os.path.exists(temp_file_path): os.unlink(temp_file_path) except Exception as e: return jsonify({"error": f"Unexpected error: {str(e)}"}), 500 @app.route('/metrics', methods=['GET']) def get_metrics(): """Get available metrics and their descriptions""" metrics_info = { "jump_height_cm": "Vertical jump height in centimeters", "flight_time_s": "Time spent in the air during the jump", "normalized_rise": "Jump height as a percentage of body height", "peak_power_watts": "Maximum power output during takeoff", "peak_force_n": "Maximum force generated during takeoff", "impulse_ns": "Total impulse (force × time) during takeoff", "rate_of_force_development": "Speed of force generation (explosiveness)", "takeoff_phase_duration_s": "Time from crouch position to launch", "ground_contact_time_s": "Time spent in contact with ground during takeoff", "frames": "Total number of video frames processed", "fps": "Video frame rate (frames per second)" } return jsonify({ "success": True, "metrics": metrics_info, "description": "Available biomechanical metrics for jump analysis" }) @app.errorhandler(404) def not_found(error): return jsonify({"error": "Endpoint not found"}), 404 @app.errorhandler(500) def internal_error(error): return jsonify({"error": "Internal server error"}), 500 if __name__ == '__main__': # Get port from environment variable or use default port = int(os.environ.get('PORT', 5000)) # Run the Flask app app.run(host='0.0.0.0', port=port, debug=True)