Spaces:
Runtime error
Runtime error
| 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 | |
| def health_check(): | |
| """Health check endpoint""" | |
| return jsonify({"status": "healthy", "message": "Athletic Performance API is running"}) | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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" | |
| }) | |
| def not_found(error): | |
| return jsonify({"error": "Endpoint not found"}), 404 | |
| 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) | |