Spaces:
Sleeping
title: Live Fitness Trainer
emoji: 💪
colorFrom: blue
colorTo: green
sdk: docker
app_file: app.py
health_check_path: /healthz
app_port: 8080
pinned: false
Fitness Trainer with Pose Estimation
An AI-powered web application that tracks exercises using computer vision and provides real-time feedback. This version now includes real-time feedback via WebSockets for an enhanced live training experience.
Features
- Real-time pose estimation using MediaPipe.
- Multiple exercise types: Squats, Push-ups, and Hammer Curls.
- Customizable sets and repetitions (via UI or API parameters).
- Exercise form feedback.
- Progress tracking (if database features are enabled).
- Web interface for easy access, including a live test page using WebSockets.
- HTTP API for single frame analysis and stateful exercise tracking.
- WebSocket API for low-latency, interactive exercise tracking.
Installation
Clone the repository:
git clone https://github.com/yourusername/fitness-trainer-pose-estimation.git # Replace with actual repo URL cd fitness-trainer-pose-estimationInstall dependencies:
pip install -r requirements.txtThis will install all necessary Python packages, including Flask, Flask-SocketIO, Gunicorn, eventlet, OpenCV, and MediaPipe.
Set up the static folder structure (if not already present):
mkdir -p static/imagesAdd exercise images to the
static/imagesfolder (if using the original UI templates):squat.pngpush_up.pnghammer_curl.png
Usage
Running Locally (Development)
Start the Flask-SocketIO development server:
python app.pyThis runs the app with Flask's built-in server, suitable for development. It will typically be available at
http://127.0.0.1:5000.Access the application:
- Main UI (if developed): Open a web browser and navigate to
http://127.0.0.1:5000/. - Live WebSocket Test Page:
- To use
live_test.html, it's recommended to serve it via a Flask route. You can add a simple route toapp.pylike this:And move@app.route('/live') def live_test_page(): return render_template('live_test.html')live_test.htmlinto thetemplatesfolder. Then access it viahttp://127.0.0.1:5000/live. - Alternatively, if you prefer to keep
live_test.htmlin the root and test without modifyingapp.pyfor serving it, you might be able to open thelive_test.htmlfile directly in your browser. However, ensure the Flask server (python app.py) is running, as the JavaScript inlive_test.htmlneeds to connect to the WebSocket server atws://127.0.0.1:5000. Relative paths to static assets like CSS or JS within the HTML might not work as expected if opened as a direct file, which is why serving it via Flask is more robust.
- To use
- Main UI (if developed): Open a web browser and navigate to
Using the Live Test Page (
live_test.html):- Select an exercise type from the dropdown.
- Click "Start Trainer".
- Allow webcam access if prompted.
- Position yourself in front of your camera so that your full body is visible.
- Observe the real-time feedback (reps, stage, messages) updated via WebSockets.
- Click "Stop Trainer" to end the session.
API Usage
The application offers two primary ways to interact with its pose estimation capabilities: a traditional HTTP API and a real-time WebSocket API.
1. HTTP API
This API is suitable for scenarios where you want to send individual frames or manage exercise sessions via HTTP requests.
a. Stateless Single-Frame Analysis (/api/analyze_frame)
Analyzes a single image frame for pose landmarks without maintaining state.
- URL:
http://127.0.0.1:5000/api/analyze_frame - Method:
POST - Payload (JSON):
image(string): Base64 encoded image.exercise_type(string, optional): "squat", "push_up", "hammer_curl". Defaults to "squat".
- Success Response (JSON):
{"success": true, "landmarks": [...]} - Error Response (JSON):
{"error": "Error message"}
b. Stateful HTTP Exercise Tracking (/api/track_exercise_stream and /api/end_exercise_session)
These endpoints allow tracking an exercise over a series of frames, maintaining state on the server.
/api/track_exercise_stream(POST):- Payload (JSON):
session_id(string): Unique ID for the session.exercise_type(string): Exercise type.image(string): Base64 encoded image of the current frame.frame_width(int): Width of the video frame.frame_height(int): Height of the video frame.
- Success Response (JSON): Contains
success,landmarks_detected, and exercise-specificdata.
- Payload (JSON):
/api/end_exercise_session(POST):- Payload (JSON):
session_id(string). - Success Response (JSON): Confirmation message.
- Payload (JSON):
(For detailed request/response structures of the HTTP API, refer to the previous README version or inspect the app.py code.)
2. WebSocket API for Live Training
This API provides a low-latency, bidirectional communication channel for real-time exercise tracking, as used by live_test.html.
Endpoint Connection: The client connects to the server's root URL, e.g.,
ws://127.0.0.1:5000/(orwss://your-deployed-app.com/). Flask-SocketIO handles this on the default namespace.Client-side Setup:
- Include the Socket.IO client library in your HTML:
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> - Connect to the server:
const socket = io(); // Or io.connect('http://your-server-address');
- Include the Socket.IO client library in your HTML:
Key WebSocket Events:
Client to Server:
socket.emit('start_exercise_session', { exercise_type: 'squat' });- Call this to initiate a new exercise session.
exercise_typecan be 'squat', 'push_up', or 'hammer_curl'.
socket.emit('process_frame', { image: 'base64img_data', exercise_type: 'squat', frame_width: width, frame_height: height });- Send a video frame for processing.
image: Base64 encoded string of the JPEG frame.exercise_type: The current exercise.frame_width,frame_height: Dimensions of the original video frame.
Server to Client:
socket.on('session_started', (data) => { ... });- Confirmation that the session has started.
datatypically includes:{ session_id: 'server_assigned_sid', exercise_type: 'squat' }. Therequest.sidon the server is used as the session identifier for WebSocket communications.
socket.on('exercise_update', (data) => { ... });- Provides feedback for the processed frame.
datastructure:{ "success": true, "landmarks_detected": true, // or false "data": { // Structure depends on the exercise_type (matches HTTP API's track_exercise_stream response data) // Example for 'squat': // "counter": 0, "stage": "down", "angle_left": 85.0, "feedback": "Good depth!" // Example for 'push_up': // "counter": 0, "stage": "up", "angle_body_left": 170.0, "feedback": "Keep body straight." // Example for 'hammer_curl': // "counter_left": 1, "stage_left": "up", "angle_left_curl": 45.0, "feedback_left": "Good curl!", ... (and right side) }, "message": "Optional message, e.g., 'No landmarks detected.'" // if landmarks_detected is false }
socket.on('session_error', (data) => { ... });- Sent if there's an error starting a session (e.g., invalid exercise type, server busy).
data:{ error: "Error message description" }
socket.on('frame_error', (data) => { ... });- Sent if there's an error processing a specific frame (e.g., bad image data, unexpected server error).
data:{ error: "Error message description" }
socket.on('disconnect', () => { ... });- Standard Socket.IO event. The client is disconnected. Server-side, the
disconnecthandler inapp.pycleans up theactive_exercise_sessionsentry for that client (request.sid).
- Standard Socket.IO event. The client is disconnected. Server-side, the
Project Structure
app.py- Main Flask application with HTTP routes and SocketIO event handlers.templates/- HTML templates (e.g.,index.html,live_test.htmlif moved here).static/- CSS, client-side JavaScript (e.g.,websocket_trainer.js), and images.pose_estimation/- Pose estimation modules.exercises/- Exercise tracking classes.feedback/- User feedback modules.utils/- Helper functions and utilities.Dockerfile- For building the application container.requirements.txt- Python dependencies.live_test.html- Client-side example for testing WebSocket functionality (currently in root, consider moving totemplates/and serving via a route).
Technologies Used
- Flask - Web framework.
- Flask-SocketIO - For WebSocket communication.
- Gunicorn + eventlet - For concurrent WSGI server suitable for WebSockets.
- OpenCV - Computer vision tasks.
- MediaPipe - Pose estimation.
- HTML/CSS/JavaScript - Frontend.
Deployment
Containerization with Docker is highly recommended for deployment.
Dockerfile
The provided Dockerfile sets up the Python environment, installs dependencies, and configures Gunicorn to run the application.
Key change for WebSockets: The Dockerfile now uses eventlet as the worker class for Gunicorn, which is essential for Flask-SocketIO:
# ... (other Dockerfile instructions) ...
# Run app.py when the container launches using Gunicorn WSGI server
CMD ["gunicorn", "--worker-class", "eventlet", "-w", "1", "--bind", "0.0.0.0:${PORT}", "--timeout", "0", "app:app"]
--worker-class eventlet: Uses the asynchronous eventlet worker.-w 1: Starts 1 worker process. For eventlet, a single worker can handle many concurrent connections.--timeout 0: Disables worker timeouts, important for long-lived WebSocket connections.
Hosting Options
1. Hugging Face Spaces
- Setup: Push your code (including the
Dockerfileandapp.py) to a Hugging Face Space repository. Select "Docker" as the Space SDK. - WebSocket Support: Hugging Face Spaces generally supports WebSockets. The application should be accessible.
- Port Configuration: Ensure your
CMDin the Dockerfile uses a port variable like${PORT}(which HF Spaces sets, often to 7860 or 8080) or a fixed port that HF Spaces can map. Theapp_port: 8080in the README metadata is a common choice.
2. Google Cloud Run
- Setup: Build your Docker container and push it to Google Container Registry (GCR) or Artifact Registry. Deploy the image from there to Cloud Run.
- WebSocket Support & Session Affinity:
- Cloud Run supports WebSockets.
- Session Affinity: For applications using Flask-SocketIO with its default in-memory session management (
active_exercise_sessionsin this app, keyed byrequest.sid), session affinity is crucial if you scale to more than one instance. Session affinity directs requests from a specific client to the same container instance. Enable it in your Cloud Run service settings. - Statelessness Consideration: While session affinity helps, be aware that
active_exercise_sessionsare still tied to individual instances. If an instance restarts or you scale down and up, that specific instance's memory is lost. For more robust session management across multiple instances or instance restarts, an external store (like Redis via Memorystore) foractive_exercise_sessionswould be necessary. For the current implementation, session affinity is the primary mechanism to make WebSockets work with multiple instances.
- Gunicorn Command: The
CMDin the Dockerfile is already configured for Cloud Run (binds to0.0.0.0:${PORT}).
3. Other PaaS/VPS
- Ensure the platform supports Python and Docker (or allows manual setup of Python, eventlet, Gunicorn).
- Configure your reverse proxy (like Nginx) correctly to handle WebSocket connections (Upgrade and Connection headers) if you place one in front of Gunicorn.
Note on Session Management (General)
The current application uses an in-memory Python dictionary (active_exercise_sessions) to store user session data, keyed by the WebSocket request.sid. This is simple but has limitations:
- Scalability: Data is not shared across multiple Gunicorn worker processes (if you were to increase
-wbeyond 1, though witheventletone worker is standard) or multiple server instances (e.g., when load balancing or on platforms like Cloud Run with multiple instances). - Persistence: Sessions are lost if the server or worker restarts. For production environments requiring high availability or scalability, consider implementing session management using an external store like Redis or Memcached.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request. Ensure your contributions align with the project's structure and coding style. If adding new features, please update relevant documentation and consider adding tests.
License
This project is licensed under the MIT License - see the LICENSE file for details (if one exists in the repository).