Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -14,9 +14,9 @@ import logging
|
|
| 14 |
from flask_cors import CORS
|
| 15 |
|
| 16 |
# Set up logging
|
| 17 |
-
logging.basicConfig(level=logging.DEBUG,
|
| 18 |
-
|
| 19 |
-
|
| 20 |
logger = logging.getLogger(__name__)
|
| 21 |
|
| 22 |
# Import attempt with error handling
|
|
@@ -41,7 +41,7 @@ try:
|
|
| 41 |
logger.info("Successfully initialized workout logger")
|
| 42 |
except ImportError:
|
| 43 |
logger.warning("WorkoutLogger import failed, creating dummy class")
|
| 44 |
-
|
| 45 |
class DummyWorkoutLogger:
|
| 46 |
def __init__(self):
|
| 47 |
pass
|
|
@@ -55,7 +55,7 @@ except ImportError:
|
|
| 55 |
return {}
|
| 56 |
def get_user_stats(self, *args, **kwargs):
|
| 57 |
return {'total_workouts': 0, 'total_exercises': 0, 'streak_days': 0}
|
| 58 |
-
|
| 59 |
workout_logger = DummyWorkoutLogger()
|
| 60 |
|
| 61 |
logger.info("Setting up Flask application")
|
|
@@ -67,7 +67,7 @@ socketio = SocketIO(app, cors_allowed_origins="*")
|
|
| 67 |
pose_estimator_api = None
|
| 68 |
active_exercise_sessions = {}
|
| 69 |
# Max number of concurrent sessions to avoid memory issues
|
| 70 |
-
MAX_SESSIONS = 100
|
| 71 |
|
| 72 |
def get_pose_estimator_api_instance():
|
| 73 |
global pose_estimator_api
|
|
@@ -104,45 +104,45 @@ def release_camera():
|
|
| 104 |
def generate_frames():
|
| 105 |
global output_frame, lock, exercise_running, current_exercise, current_exercise_data
|
| 106 |
global exercise_counter, exercise_goal, sets_completed, sets_goal
|
| 107 |
-
|
| 108 |
pose_estimator = PoseEstimator()
|
| 109 |
-
|
| 110 |
while True:
|
| 111 |
if camera is None:
|
| 112 |
continue
|
| 113 |
-
|
| 114 |
success, frame = camera.read()
|
| 115 |
if not success:
|
| 116 |
continue
|
| 117 |
-
|
| 118 |
# Only process frames if an exercise is running
|
| 119 |
if exercise_running and current_exercise:
|
| 120 |
# Process with pose estimation
|
| 121 |
results = pose_estimator.estimate_pose(frame, current_exercise_data['type'])
|
| 122 |
-
|
| 123 |
if results.pose_landmarks:
|
| 124 |
# Track exercise based on type
|
| 125 |
if current_exercise_data['type'] == "squat":
|
| 126 |
counter, angle, stage = current_exercise.track_squat(results.pose_landmarks.landmark, frame)
|
| 127 |
layout_indicators(frame, current_exercise_data['type'], (counter, angle, stage))
|
| 128 |
exercise_counter = counter
|
| 129 |
-
|
| 130 |
elif current_exercise_data['type'] == "push_up":
|
| 131 |
counter, angle, stage = current_exercise.track_push_up(results.pose_landmarks.landmark, frame)
|
| 132 |
layout_indicators(frame, current_exercise_data['type'], (counter, angle, stage))
|
| 133 |
exercise_counter = counter
|
| 134 |
-
|
| 135 |
elif current_exercise_data['type'] == "hammer_curl":
|
| 136 |
(counter_right, angle_right, counter_left, angle_left,
|
| 137 |
-
warning_message_right, warning_message_left, progress_right,
|
| 138 |
progress_left, stage_right, stage_left) = current_exercise.track_hammer_curl(
|
| 139 |
results.pose_landmarks.landmark, frame)
|
| 140 |
-
layout_indicators(frame, current_exercise_data['type'],
|
| 141 |
(counter_right, angle_right, counter_left, angle_left,
|
| 142 |
-
warning_message_right, warning_message_left,
|
| 143 |
progress_right, progress_left, stage_right, stage_left))
|
| 144 |
exercise_counter = max(counter_right, counter_left)
|
| 145 |
-
|
| 146 |
# Display exercise information
|
| 147 |
exercise_info = get_exercise_info(current_exercise_data['type'])
|
| 148 |
draw_text_with_background(frame, f"Exercise: {exercise_info.get('name', 'N/A')}", (40, 50),
|
|
@@ -153,7 +153,7 @@ def generate_frames():
|
|
| 153 |
cv2.FONT_HERSHEY_DUPLEX, 0.7, (255, 255, 255), (118, 29, 14), 1)
|
| 154 |
draw_text_with_background(frame, f"Current Set: {sets_completed + 1}", (40, 140),
|
| 155 |
cv2.FONT_HERSHEY_DUPLEX, 0.7, (255, 255, 255), (118, 29, 14), 1)
|
| 156 |
-
|
| 157 |
# Check if rep goal is reached for current set
|
| 158 |
if exercise_counter >= exercise_goal:
|
| 159 |
sets_completed += 1
|
|
@@ -164,14 +164,14 @@ def generate_frames():
|
|
| 164 |
elif current_exercise_data['type'] == "hammer_curl":
|
| 165 |
current_exercise.counter_right = 0
|
| 166 |
current_exercise.counter_left = 0
|
| 167 |
-
|
| 168 |
# Check if all sets are completed
|
| 169 |
if sets_completed >= sets_goal:
|
| 170 |
exercise_running = False
|
| 171 |
draw_text_with_background(frame, "WORKOUT COMPLETE!", (frame.shape[1]//2 - 150, frame.shape[0]//2),
|
| 172 |
cv2.FONT_HERSHEY_DUPLEX, 1.2, (255, 255, 255), (0, 200, 0), 2)
|
| 173 |
else:
|
| 174 |
-
draw_text_with_background(frame, f"SET {sets_completed} COMPLETE! Rest for 30 sec",
|
| 175 |
(frame.shape[1]//2 - 200, frame.shape[0]//2),
|
| 176 |
cv2.FONT_HERSHEY_DUPLEX, 1.0, (255, 255, 255), (0, 0, 200), 2)
|
| 177 |
# We could add rest timer functionality here
|
|
@@ -179,11 +179,11 @@ def generate_frames():
|
|
| 179 |
# Display welcome message if no exercise is running
|
| 180 |
cv2.putText(frame, "Select an exercise to begin", (frame.shape[1]//2 - 150, frame.shape[0]//2),
|
| 181 |
cv2.FONT_HERSHEY_DUPLEX, 0.8, (255, 255, 255), 1)
|
| 182 |
-
|
| 183 |
# Encode the frame in JPEG format
|
| 184 |
with lock:
|
| 185 |
output_frame = frame.copy()
|
| 186 |
-
|
| 187 |
# Yield the frame in byte format
|
| 188 |
ret, buffer = cv2.imencode('.jpg', output_frame)
|
| 189 |
frame = buffer.tobytes()
|
|
@@ -210,7 +210,7 @@ def dashboard():
|
|
| 210 |
weekly_stats = workout_logger.get_weekly_stats()
|
| 211 |
exercise_distribution = workout_logger.get_exercise_distribution()
|
| 212 |
user_stats = workout_logger.get_user_stats()
|
| 213 |
-
|
| 214 |
# Format workouts for display
|
| 215 |
formatted_workouts = []
|
| 216 |
for workout in recent_workouts:
|
|
@@ -221,10 +221,10 @@ def dashboard():
|
|
| 221 |
'reps': workout['reps'],
|
| 222 |
'duration': f"{workout['duration_seconds'] // 60}:{workout['duration_seconds'] % 60:02d}"
|
| 223 |
})
|
| 224 |
-
|
| 225 |
# Calculate total workouts this week
|
| 226 |
weekly_workout_count = sum(day['workout_count'] for day in weekly_stats.values())
|
| 227 |
-
|
| 228 |
return render_template('dashboard.html',
|
| 229 |
recent_workouts=formatted_workouts,
|
| 230 |
weekly_workouts=weekly_workout_count,
|
|
@@ -248,20 +248,20 @@ def start_exercise():
|
|
| 248 |
global exercise_running, current_exercise, current_exercise_data
|
| 249 |
global exercise_counter, exercise_goal, sets_completed, sets_goal
|
| 250 |
global workout_start_time
|
| 251 |
-
|
| 252 |
data = request.json
|
| 253 |
exercise_type = data.get('exercise_type')
|
| 254 |
sets_goal = int(data.get('sets', 3))
|
| 255 |
exercise_goal = int(data.get('reps', 10))
|
| 256 |
-
|
| 257 |
# Initialize camera if not already done
|
| 258 |
initialize_camera()
|
| 259 |
-
|
| 260 |
# Reset counters
|
| 261 |
exercise_counter = 0
|
| 262 |
sets_completed = 0
|
| 263 |
workout_start_time = time.time()
|
| 264 |
-
|
| 265 |
# Initialize the appropriate exercise class
|
| 266 |
if exercise_type == "squat":
|
| 267 |
current_exercise = Squat()
|
|
@@ -271,17 +271,17 @@ def start_exercise():
|
|
| 271 |
current_exercise = HammerCurl()
|
| 272 |
else:
|
| 273 |
return jsonify({'success': False, 'error': 'Invalid exercise type'})
|
| 274 |
-
|
| 275 |
# Store exercise data
|
| 276 |
current_exercise_data = {
|
| 277 |
'type': exercise_type,
|
| 278 |
'sets': sets_goal,
|
| 279 |
'reps': exercise_goal
|
| 280 |
}
|
| 281 |
-
|
| 282 |
# Start the exercise
|
| 283 |
exercise_running = True
|
| 284 |
-
|
| 285 |
return jsonify({'success': True})
|
| 286 |
|
| 287 |
@app.route('/stop_exercise', methods=['POST'])
|
|
@@ -289,11 +289,11 @@ def stop_exercise():
|
|
| 289 |
"""Stop the current exercise and log the workout"""
|
| 290 |
global exercise_running, current_exercise_data, workout_start_time
|
| 291 |
global exercise_counter, exercise_goal, sets_completed, sets_goal
|
| 292 |
-
|
| 293 |
if exercise_running and current_exercise_data:
|
| 294 |
# Calculate duration
|
| 295 |
duration = int(time.time() - workout_start_time) if workout_start_time else 0
|
| 296 |
-
|
| 297 |
# Log the workout
|
| 298 |
workout_logger.log_workout(
|
| 299 |
exercise_type=current_exercise_data['type'],
|
|
@@ -309,7 +309,7 @@ def stop_exercise():
|
|
| 309 |
def get_status():
|
| 310 |
"""Return current exercise status"""
|
| 311 |
global exercise_counter, sets_completed, exercise_goal, sets_goal, exercise_running
|
| 312 |
-
|
| 313 |
return jsonify({
|
| 314 |
'exercise_running': exercise_running,
|
| 315 |
'current_reps': exercise_counter,
|
|
@@ -325,7 +325,7 @@ def profile():
|
|
| 325 |
|
| 326 |
@app.route('/healthz')
|
| 327 |
def health_check():
|
| 328 |
-
logger.info("
|
| 329 |
return "OK", 200
|
| 330 |
|
| 331 |
@app.route('/api/analyze_frame', methods=['POST'])
|
|
@@ -336,7 +336,7 @@ def analyze_frame():
|
|
| 336 |
if not data or 'image' not in data:
|
| 337 |
logger.warning("API /api/analyze_frame: No image provided or invalid JSON.")
|
| 338 |
return jsonify({'error': 'No image provided in JSON payload (expected base64 string under "image" key)'}), 400
|
| 339 |
-
|
| 340 |
# Use 'squat' as a default if not provided. This primarily affects server-side drawing, not the landmarks themselves.
|
| 341 |
exercise_type_for_api = data.get('exercise_type', 'squat')
|
| 342 |
if exercise_type_for_api not in ["squat", "push_up", "hammer_curl"]: # Validate against known types if necessary
|
|
@@ -344,7 +344,7 @@ def analyze_frame():
|
|
| 344 |
exercise_type_for_api = "squat"
|
| 345 |
|
| 346 |
image_data = data['image']
|
| 347 |
-
|
| 348 |
# Add padding if missing for base64 decoding
|
| 349 |
missing_padding = len(image_data) % 4
|
| 350 |
if missing_padding:
|
|
@@ -361,10 +361,10 @@ def analyze_frame():
|
|
| 361 |
return jsonify({'error': f'Invalid base64 image data: {str(e)}'}), 400
|
| 362 |
|
| 363 |
estimator = get_pose_estimator_api_instance()
|
| 364 |
-
|
| 365 |
# Process with pose estimation
|
| 366 |
-
results = estimator.estimate_pose(frame, exercise_type_for_api)
|
| 367 |
-
|
| 368 |
landmarks_list = []
|
| 369 |
if results.pose_landmarks:
|
| 370 |
for i, landmark in enumerate(results.pose_landmarks.landmark):
|
|
@@ -406,7 +406,7 @@ def track_exercise_stream():
|
|
| 406 |
|
| 407 |
if not isinstance(frame_width, int) or not isinstance(frame_height, int) or frame_width <= 0 or frame_height <= 0:
|
| 408 |
return jsonify({'error': 'Invalid frame_width or frame_height'}), 400
|
| 409 |
-
|
| 410 |
# Manage session limit
|
| 411 |
if len(active_exercise_sessions) >= MAX_SESSIONS and session_id not in active_exercise_sessions:
|
| 412 |
logger.warning(f"Max sessions ({MAX_SESSIONS}) reached. Rejecting new session {session_id}.")
|
|
@@ -426,7 +426,7 @@ def track_exercise_stream():
|
|
| 426 |
else:
|
| 427 |
logger.warning(f"Invalid exercise type: {exercise_type} for session {session_id}")
|
| 428 |
return jsonify({'error': 'Invalid exercise_type'}), 400
|
| 429 |
-
|
| 430 |
exercise_session = active_exercise_sessions[session_id]
|
| 431 |
|
| 432 |
# Decode image (similar to /api/analyze_frame)
|
|
@@ -445,18 +445,18 @@ def track_exercise_stream():
|
|
| 445 |
pose_estimator = get_pose_estimator_api_instance() # Reusing the existing estimator instance getter
|
| 446 |
# The 'squat' here is just a default for PoseEstimator's internal drawing logic, which we don't use for API response.
|
| 447 |
# The actual exercise type for tracking is handled by `exercise_session` object.
|
| 448 |
-
results = pose_estimator.estimate_pose(frame_for_estimation, 'squat')
|
| 449 |
|
| 450 |
if not results.pose_landmarks:
|
| 451 |
logger.info(f"Session {session_id}: No landmarks detected.")
|
| 452 |
# Return current state even if no new landmarks, or specific message
|
| 453 |
return jsonify({
|
| 454 |
-
'success': True,
|
| 455 |
'landmarks_detected': False,
|
| 456 |
'message': 'No landmarks detected in this frame.',
|
| 457 |
# Optionally, could return last known state from exercise_session if needed
|
| 458 |
})
|
| 459 |
-
|
| 460 |
exercise_data = None
|
| 461 |
if exercise_type == 'squat':
|
| 462 |
exercise_data = exercise_session.track_squat(results.pose_landmarks.landmark, frame_width, frame_height)
|
|
@@ -464,7 +464,7 @@ def track_exercise_stream():
|
|
| 464 |
exercise_data = exercise_session.track_push_up(results.pose_landmarks.landmark, frame_width, frame_height)
|
| 465 |
elif exercise_type == 'hammer_curl':
|
| 466 |
exercise_data = exercise_session.track_hammer_curl(results.pose_landmarks.landmark, frame_width, frame_height)
|
| 467 |
-
|
| 468 |
if exercise_data:
|
| 469 |
logger.debug(f"Session {session_id}: Exercise data: {exercise_data}")
|
| 470 |
return jsonify({'success': True, 'landmarks_detected': True, 'data': exercise_data})
|
|
@@ -486,7 +486,7 @@ def end_exercise_session():
|
|
| 486 |
data = request.json
|
| 487 |
if not data:
|
| 488 |
return jsonify({'error': 'No JSON data provided'}), 400
|
| 489 |
-
|
| 490 |
session_id = data.get('session_id')
|
| 491 |
if not session_id:
|
| 492 |
return jsonify({'error': 'Missing session_id'}), 400
|
|
@@ -547,7 +547,7 @@ def handle_start_exercise_session(data):
|
|
| 547 |
logger.warning(f"Invalid exercise type: {exercise_type} for session {request.sid}")
|
| 548 |
socketio.emit('session_error', {'error': 'Invalid exercise_type'}, room=request.sid)
|
| 549 |
return
|
| 550 |
-
|
| 551 |
logger.info(f"Successfully created exercise session for {request.sid}, type: {exercise_type}")
|
| 552 |
socketio.emit('session_started', {'session_id': request.sid, 'exercise_type': exercise_type}, room=request.sid)
|
| 553 |
|
|
@@ -579,43 +579,42 @@ def handle_process_frame(data):
|
|
| 579 |
return
|
| 580 |
|
| 581 |
try:
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
|
|
|
|
|
|
| 593 |
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
|
|
|
|
|
|
| 610 |
else:
|
| 611 |
-
|
| 612 |
-
logger.error(f"Could not get exercise data for {request.sid}, type {session_exercise_type}, despite landmarks.")
|
| 613 |
-
socketio.emit('exercise_update', {'success': False, 'landmarks_detected': True, 'message': 'Error processing landmarks.'}, room=request.sid)
|
| 614 |
-
else:
|
| 615 |
-
# logger.info(f"No landmarks detected for {request.sid}.")
|
| 616 |
-
socketio.emit('exercise_update', {'success': True, 'landmarks_detected': False, 'message': 'No landmarks detected.'}, room=request.sid)
|
| 617 |
|
| 618 |
-
except Exception as e:
|
| 619 |
logger.error(f"Error processing frame for {request.sid}: {e}")
|
| 620 |
traceback.print_exc()
|
| 621 |
socketio.emit('frame_error', {'error': f'Internal server error during frame processing: {str(e)}'}, room=request.sid)
|
|
@@ -629,4 +628,4 @@ if __name__ == '__main__':
|
|
| 629 |
socketio.run(app, debug=True, host='0.0.0.0', port=5000)
|
| 630 |
except Exception as e:
|
| 631 |
logger.error(f"Failed to start application: {e}")
|
| 632 |
-
traceback.print_exc()
|
|
|
|
| 14 |
from flask_cors import CORS
|
| 15 |
|
| 16 |
# Set up logging
|
| 17 |
+
logging.basicConfig(level=logging.DEBUG,
|
| 18 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 19 |
+
handlers=[logging.StreamHandler()])
|
| 20 |
logger = logging.getLogger(__name__)
|
| 21 |
|
| 22 |
# Import attempt with error handling
|
|
|
|
| 41 |
logger.info("Successfully initialized workout logger")
|
| 42 |
except ImportError:
|
| 43 |
logger.warning("WorkoutLogger import failed, creating dummy class")
|
| 44 |
+
|
| 45 |
class DummyWorkoutLogger:
|
| 46 |
def __init__(self):
|
| 47 |
pass
|
|
|
|
| 55 |
return {}
|
| 56 |
def get_user_stats(self, *args, **kwargs):
|
| 57 |
return {'total_workouts': 0, 'total_exercises': 0, 'streak_days': 0}
|
| 58 |
+
|
| 59 |
workout_logger = DummyWorkoutLogger()
|
| 60 |
|
| 61 |
logger.info("Setting up Flask application")
|
|
|
|
| 67 |
pose_estimator_api = None
|
| 68 |
active_exercise_sessions = {}
|
| 69 |
# Max number of concurrent sessions to avoid memory issues
|
| 70 |
+
MAX_SESSIONS = 100
|
| 71 |
|
| 72 |
def get_pose_estimator_api_instance():
|
| 73 |
global pose_estimator_api
|
|
|
|
| 104 |
def generate_frames():
|
| 105 |
global output_frame, lock, exercise_running, current_exercise, current_exercise_data
|
| 106 |
global exercise_counter, exercise_goal, sets_completed, sets_goal
|
| 107 |
+
|
| 108 |
pose_estimator = PoseEstimator()
|
| 109 |
+
|
| 110 |
while True:
|
| 111 |
if camera is None:
|
| 112 |
continue
|
| 113 |
+
|
| 114 |
success, frame = camera.read()
|
| 115 |
if not success:
|
| 116 |
continue
|
| 117 |
+
|
| 118 |
# Only process frames if an exercise is running
|
| 119 |
if exercise_running and current_exercise:
|
| 120 |
# Process with pose estimation
|
| 121 |
results = pose_estimator.estimate_pose(frame, current_exercise_data['type'])
|
| 122 |
+
|
| 123 |
if results.pose_landmarks:
|
| 124 |
# Track exercise based on type
|
| 125 |
if current_exercise_data['type'] == "squat":
|
| 126 |
counter, angle, stage = current_exercise.track_squat(results.pose_landmarks.landmark, frame)
|
| 127 |
layout_indicators(frame, current_exercise_data['type'], (counter, angle, stage))
|
| 128 |
exercise_counter = counter
|
| 129 |
+
|
| 130 |
elif current_exercise_data['type'] == "push_up":
|
| 131 |
counter, angle, stage = current_exercise.track_push_up(results.pose_landmarks.landmark, frame)
|
| 132 |
layout_indicators(frame, current_exercise_data['type'], (counter, angle, stage))
|
| 133 |
exercise_counter = counter
|
| 134 |
+
|
| 135 |
elif current_exercise_data['type'] == "hammer_curl":
|
| 136 |
(counter_right, angle_right, counter_left, angle_left,
|
| 137 |
+
warning_message_right, warning_message_left, progress_right,
|
| 138 |
progress_left, stage_right, stage_left) = current_exercise.track_hammer_curl(
|
| 139 |
results.pose_landmarks.landmark, frame)
|
| 140 |
+
layout_indicators(frame, current_exercise_data['type'],
|
| 141 |
(counter_right, angle_right, counter_left, angle_left,
|
| 142 |
+
warning_message_right, warning_message_left,
|
| 143 |
progress_right, progress_left, stage_right, stage_left))
|
| 144 |
exercise_counter = max(counter_right, counter_left)
|
| 145 |
+
|
| 146 |
# Display exercise information
|
| 147 |
exercise_info = get_exercise_info(current_exercise_data['type'])
|
| 148 |
draw_text_with_background(frame, f"Exercise: {exercise_info.get('name', 'N/A')}", (40, 50),
|
|
|
|
| 153 |
cv2.FONT_HERSHEY_DUPLEX, 0.7, (255, 255, 255), (118, 29, 14), 1)
|
| 154 |
draw_text_with_background(frame, f"Current Set: {sets_completed + 1}", (40, 140),
|
| 155 |
cv2.FONT_HERSHEY_DUPLEX, 0.7, (255, 255, 255), (118, 29, 14), 1)
|
| 156 |
+
|
| 157 |
# Check if rep goal is reached for current set
|
| 158 |
if exercise_counter >= exercise_goal:
|
| 159 |
sets_completed += 1
|
|
|
|
| 164 |
elif current_exercise_data['type'] == "hammer_curl":
|
| 165 |
current_exercise.counter_right = 0
|
| 166 |
current_exercise.counter_left = 0
|
| 167 |
+
|
| 168 |
# Check if all sets are completed
|
| 169 |
if sets_completed >= sets_goal:
|
| 170 |
exercise_running = False
|
| 171 |
draw_text_with_background(frame, "WORKOUT COMPLETE!", (frame.shape[1]//2 - 150, frame.shape[0]//2),
|
| 172 |
cv2.FONT_HERSHEY_DUPLEX, 1.2, (255, 255, 255), (0, 200, 0), 2)
|
| 173 |
else:
|
| 174 |
+
draw_text_with_background(frame, f"SET {sets_completed} COMPLETE! Rest for 30 sec",
|
| 175 |
(frame.shape[1]//2 - 200, frame.shape[0]//2),
|
| 176 |
cv2.FONT_HERSHEY_DUPLEX, 1.0, (255, 255, 255), (0, 0, 200), 2)
|
| 177 |
# We could add rest timer functionality here
|
|
|
|
| 179 |
# Display welcome message if no exercise is running
|
| 180 |
cv2.putText(frame, "Select an exercise to begin", (frame.shape[1]//2 - 150, frame.shape[0]//2),
|
| 181 |
cv2.FONT_HERSHEY_DUPLEX, 0.8, (255, 255, 255), 1)
|
| 182 |
+
|
| 183 |
# Encode the frame in JPEG format
|
| 184 |
with lock:
|
| 185 |
output_frame = frame.copy()
|
| 186 |
+
|
| 187 |
# Yield the frame in byte format
|
| 188 |
ret, buffer = cv2.imencode('.jpg', output_frame)
|
| 189 |
frame = buffer.tobytes()
|
|
|
|
| 210 |
weekly_stats = workout_logger.get_weekly_stats()
|
| 211 |
exercise_distribution = workout_logger.get_exercise_distribution()
|
| 212 |
user_stats = workout_logger.get_user_stats()
|
| 213 |
+
|
| 214 |
# Format workouts for display
|
| 215 |
formatted_workouts = []
|
| 216 |
for workout in recent_workouts:
|
|
|
|
| 221 |
'reps': workout['reps'],
|
| 222 |
'duration': f"{workout['duration_seconds'] // 60}:{workout['duration_seconds'] % 60:02d}"
|
| 223 |
})
|
| 224 |
+
|
| 225 |
# Calculate total workouts this week
|
| 226 |
weekly_workout_count = sum(day['workout_count'] for day in weekly_stats.values())
|
| 227 |
+
|
| 228 |
return render_template('dashboard.html',
|
| 229 |
recent_workouts=formatted_workouts,
|
| 230 |
weekly_workouts=weekly_workout_count,
|
|
|
|
| 248 |
global exercise_running, current_exercise, current_exercise_data
|
| 249 |
global exercise_counter, exercise_goal, sets_completed, sets_goal
|
| 250 |
global workout_start_time
|
| 251 |
+
|
| 252 |
data = request.json
|
| 253 |
exercise_type = data.get('exercise_type')
|
| 254 |
sets_goal = int(data.get('sets', 3))
|
| 255 |
exercise_goal = int(data.get('reps', 10))
|
| 256 |
+
|
| 257 |
# Initialize camera if not already done
|
| 258 |
initialize_camera()
|
| 259 |
+
|
| 260 |
# Reset counters
|
| 261 |
exercise_counter = 0
|
| 262 |
sets_completed = 0
|
| 263 |
workout_start_time = time.time()
|
| 264 |
+
|
| 265 |
# Initialize the appropriate exercise class
|
| 266 |
if exercise_type == "squat":
|
| 267 |
current_exercise = Squat()
|
|
|
|
| 271 |
current_exercise = HammerCurl()
|
| 272 |
else:
|
| 273 |
return jsonify({'success': False, 'error': 'Invalid exercise type'})
|
| 274 |
+
|
| 275 |
# Store exercise data
|
| 276 |
current_exercise_data = {
|
| 277 |
'type': exercise_type,
|
| 278 |
'sets': sets_goal,
|
| 279 |
'reps': exercise_goal
|
| 280 |
}
|
| 281 |
+
|
| 282 |
# Start the exercise
|
| 283 |
exercise_running = True
|
| 284 |
+
|
| 285 |
return jsonify({'success': True})
|
| 286 |
|
| 287 |
@app.route('/stop_exercise', methods=['POST'])
|
|
|
|
| 289 |
"""Stop the current exercise and log the workout"""
|
| 290 |
global exercise_running, current_exercise_data, workout_start_time
|
| 291 |
global exercise_counter, exercise_goal, sets_completed, sets_goal
|
| 292 |
+
|
| 293 |
if exercise_running and current_exercise_data:
|
| 294 |
# Calculate duration
|
| 295 |
duration = int(time.time() - workout_start_time) if workout_start_time else 0
|
| 296 |
+
|
| 297 |
# Log the workout
|
| 298 |
workout_logger.log_workout(
|
| 299 |
exercise_type=current_exercise_data['type'],
|
|
|
|
| 309 |
def get_status():
|
| 310 |
"""Return current exercise status"""
|
| 311 |
global exercise_counter, sets_completed, exercise_goal, sets_goal, exercise_running
|
| 312 |
+
|
| 313 |
return jsonify({
|
| 314 |
'exercise_running': exercise_running,
|
| 315 |
'current_reps': exercise_counter,
|
|
|
|
| 325 |
|
| 326 |
@app.route('/healthz')
|
| 327 |
def health_check():
|
| 328 |
+
logger.info("HEALTH CHECK ENDPOINT CALLED AND RESPONDING OK")
|
| 329 |
return "OK", 200
|
| 330 |
|
| 331 |
@app.route('/api/analyze_frame', methods=['POST'])
|
|
|
|
| 336 |
if not data or 'image' not in data:
|
| 337 |
logger.warning("API /api/analyze_frame: No image provided or invalid JSON.")
|
| 338 |
return jsonify({'error': 'No image provided in JSON payload (expected base64 string under "image" key)'}), 400
|
| 339 |
+
|
| 340 |
# Use 'squat' as a default if not provided. This primarily affects server-side drawing, not the landmarks themselves.
|
| 341 |
exercise_type_for_api = data.get('exercise_type', 'squat')
|
| 342 |
if exercise_type_for_api not in ["squat", "push_up", "hammer_curl"]: # Validate against known types if necessary
|
|
|
|
| 344 |
exercise_type_for_api = "squat"
|
| 345 |
|
| 346 |
image_data = data['image']
|
| 347 |
+
|
| 348 |
# Add padding if missing for base64 decoding
|
| 349 |
missing_padding = len(image_data) % 4
|
| 350 |
if missing_padding:
|
|
|
|
| 361 |
return jsonify({'error': f'Invalid base64 image data: {str(e)}'}), 400
|
| 362 |
|
| 363 |
estimator = get_pose_estimator_api_instance()
|
| 364 |
+
|
| 365 |
# Process with pose estimation
|
| 366 |
+
results = estimator.estimate_pose(frame, exercise_type_for_api)
|
| 367 |
+
|
| 368 |
landmarks_list = []
|
| 369 |
if results.pose_landmarks:
|
| 370 |
for i, landmark in enumerate(results.pose_landmarks.landmark):
|
|
|
|
| 406 |
|
| 407 |
if not isinstance(frame_width, int) or not isinstance(frame_height, int) or frame_width <= 0 or frame_height <= 0:
|
| 408 |
return jsonify({'error': 'Invalid frame_width or frame_height'}), 400
|
| 409 |
+
|
| 410 |
# Manage session limit
|
| 411 |
if len(active_exercise_sessions) >= MAX_SESSIONS and session_id not in active_exercise_sessions:
|
| 412 |
logger.warning(f"Max sessions ({MAX_SESSIONS}) reached. Rejecting new session {session_id}.")
|
|
|
|
| 426 |
else:
|
| 427 |
logger.warning(f"Invalid exercise type: {exercise_type} for session {session_id}")
|
| 428 |
return jsonify({'error': 'Invalid exercise_type'}), 400
|
| 429 |
+
|
| 430 |
exercise_session = active_exercise_sessions[session_id]
|
| 431 |
|
| 432 |
# Decode image (similar to /api/analyze_frame)
|
|
|
|
| 445 |
pose_estimator = get_pose_estimator_api_instance() # Reusing the existing estimator instance getter
|
| 446 |
# The 'squat' here is just a default for PoseEstimator's internal drawing logic, which we don't use for API response.
|
| 447 |
# The actual exercise type for tracking is handled by `exercise_session` object.
|
| 448 |
+
results = pose_estimator.estimate_pose(frame_for_estimation, 'squat')
|
| 449 |
|
| 450 |
if not results.pose_landmarks:
|
| 451 |
logger.info(f"Session {session_id}: No landmarks detected.")
|
| 452 |
# Return current state even if no new landmarks, or specific message
|
| 453 |
return jsonify({
|
| 454 |
+
'success': True,
|
| 455 |
'landmarks_detected': False,
|
| 456 |
'message': 'No landmarks detected in this frame.',
|
| 457 |
# Optionally, could return last known state from exercise_session if needed
|
| 458 |
})
|
| 459 |
+
|
| 460 |
exercise_data = None
|
| 461 |
if exercise_type == 'squat':
|
| 462 |
exercise_data = exercise_session.track_squat(results.pose_landmarks.landmark, frame_width, frame_height)
|
|
|
|
| 464 |
exercise_data = exercise_session.track_push_up(results.pose_landmarks.landmark, frame_width, frame_height)
|
| 465 |
elif exercise_type == 'hammer_curl':
|
| 466 |
exercise_data = exercise_session.track_hammer_curl(results.pose_landmarks.landmark, frame_width, frame_height)
|
| 467 |
+
|
| 468 |
if exercise_data:
|
| 469 |
logger.debug(f"Session {session_id}: Exercise data: {exercise_data}")
|
| 470 |
return jsonify({'success': True, 'landmarks_detected': True, 'data': exercise_data})
|
|
|
|
| 486 |
data = request.json
|
| 487 |
if not data:
|
| 488 |
return jsonify({'error': 'No JSON data provided'}), 400
|
| 489 |
+
|
| 490 |
session_id = data.get('session_id')
|
| 491 |
if not session_id:
|
| 492 |
return jsonify({'error': 'Missing session_id'}), 400
|
|
|
|
| 547 |
logger.warning(f"Invalid exercise type: {exercise_type} for session {request.sid}")
|
| 548 |
socketio.emit('session_error', {'error': 'Invalid exercise_type'}, room=request.sid)
|
| 549 |
return
|
| 550 |
+
|
| 551 |
logger.info(f"Successfully created exercise session for {request.sid}, type: {exercise_type}")
|
| 552 |
socketio.emit('session_started', {'session_id': request.sid, 'exercise_type': exercise_type}, room=request.sid)
|
| 553 |
|
|
|
|
| 579 |
return
|
| 580 |
|
| 581 |
try:
|
| 582 |
+
# This try-except block is for image decoding
|
| 583 |
+
try:
|
| 584 |
+
missing_padding = len(image_data_base64) % 4
|
| 585 |
+
if missing_padding:
|
| 586 |
+
image_data_base64 += '=' * (4 - missing_padding)
|
| 587 |
+
|
| 588 |
+
image_bytes = base64.b64decode(image_data_base64)
|
| 589 |
+
pil_image = Image.open(io.BytesIO(image_bytes))
|
| 590 |
+
frame_for_estimation = cv2.cvtColor(np.array(pil_image), cv2.COLOR_RGB2BGR)
|
| 591 |
+
except Exception as e:
|
| 592 |
+
logger.error(f"Error decoding base64 image for {request.sid}: {e}")
|
| 593 |
+
socketio.emit('frame_error', {'error': f'Invalid base64 image data: {str(e)}'}, room=request.sid)
|
| 594 |
+
return # Important to return here if decoding fails
|
| 595 |
|
| 596 |
+
# This is the main processing logic that should be wrapped in the outer try-except
|
| 597 |
+
pose_estimator = get_pose_estimator_api_instance()
|
| 598 |
+
results = pose_estimator.estimate_pose(frame_for_estimation, session_exercise_type)
|
| 599 |
+
|
| 600 |
+
exercise_data_result = None
|
| 601 |
+
if results.pose_landmarks:
|
| 602 |
+
if session_exercise_type == 'squat':
|
| 603 |
+
exercise_data_result = exercise_session.track_squat(results.pose_landmarks.landmark, frame_width, frame_height)
|
| 604 |
+
elif session_exercise_type == 'push_up':
|
| 605 |
+
exercise_data_result = exercise_session.track_push_up(results.pose_landmarks.landmark, frame_width, frame_height)
|
| 606 |
+
elif session_exercise_type == 'hammer_curl':
|
| 607 |
+
exercise_data_result = exercise_session.track_hammer_curl(results.pose_landmarks.landmark, frame_width, frame_height)
|
| 608 |
+
|
| 609 |
+
if exercise_data_result:
|
| 610 |
+
socketio.emit('exercise_update', {'success': True, 'landmarks_detected': True, 'data': exercise_data_result}, room=request.sid)
|
| 611 |
+
else:
|
| 612 |
+
logger.error(f"Could not get exercise_data for {request.sid}, type {session_exercise_type}, despite landmarks.")
|
| 613 |
+
socketio.emit('exercise_update', {'success': False, 'landmarks_detected': True, 'message': 'Error processing landmarks.'}, room=request.sid)
|
| 614 |
else:
|
| 615 |
+
socketio.emit('exercise_update', {'success': True, 'landmarks_detected': False, 'message': 'No landmarks detected.'}, room=request.sid)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 616 |
|
| 617 |
+
except Exception as e: # This is the general exception handler for the processing logic
|
| 618 |
logger.error(f"Error processing frame for {request.sid}: {e}")
|
| 619 |
traceback.print_exc()
|
| 620 |
socketio.emit('frame_error', {'error': f'Internal server error during frame processing: {str(e)}'}, room=request.sid)
|
|
|
|
| 628 |
socketio.run(app, debug=True, host='0.0.0.0', port=5000)
|
| 629 |
except Exception as e:
|
| 630 |
logger.error(f"Failed to start application: {e}")
|
| 631 |
+
traceback.print_exc()
|