pjxcharya commited on
Commit
a10a8b8
Β·
verified Β·
1 Parent(s): 501c569

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +208 -370
app.py CHANGED
@@ -3,7 +3,7 @@ import cv2
3
  import numpy as np
4
  import mediapipe as mp
5
  import time
6
- import traceback
7
 
8
  # Import your exercise classes
9
  from exercises.hammer_curl import HammerCurl
@@ -22,135 +22,19 @@ exercise_trackers = {
22
  "Push Up": PushUp(),
23
  "Squat": Squat()
24
  }
25
- # current_exercise_tracker will be set when an exercise is selected and workout starts
26
- # It's fetched using exercise_trackers.get(selected_exercise_name)
27
  selected_exercise_name = "Hammer Curl" # Default exercise
28
 
29
- target_reps = 10
30
- target_sets = 3
 
31
  current_set_count = 1
32
  workout_complete_message = ""
33
- workout_active = False # New state variable to control active workout session
34
 
35
 
36
- def update_targets_and_exercise_display(exercise_name_choice, reps_in, sets_in):
37
- """
38
- Handles changes from exercise selection, target reps, or target sets.
39
- Updates system state and prepares for a new workout session if one isn't active.
40
- Returns values to update relevant UI text components.
41
- """
42
- global selected_exercise_name, target_reps, target_sets
43
- global current_set_count, workout_complete_message, workout_active
44
-
45
- exercise_changed_or_targets_changed = False
46
-
47
- # Update selected exercise
48
- if selected_exercise_name != exercise_name_choice:
49
- selected_exercise_name = exercise_name_choice
50
- exercise_changed_or_targets_changed = True
51
- print(f"Exercise changed to: {selected_exercise_name}.")
52
-
53
- # Update target reps
54
- try:
55
- new_reps = int(reps_in)
56
- if new_reps > 0 and target_reps != new_reps:
57
- target_reps = new_reps
58
- exercise_changed_or_targets_changed = True
59
- print(f"Target reps updated to: {target_reps}.")
60
- except (ValueError, TypeError): pass # Keep old value if input is not a valid integer
61
-
62
- # Update target sets
63
- try:
64
- new_sets = int(sets_in)
65
- if new_sets > 0 and target_sets != new_sets:
66
- target_sets = new_sets
67
- exercise_changed_or_targets_changed = True
68
- print(f"Target sets updated to: {target_sets}.")
69
- except (ValueError, TypeError): pass
70
-
71
- if exercise_changed_or_targets_changed:
72
- # Reset tracker and progress as exercise or targets changed
73
- current_tracker = exercise_trackers.get(selected_exercise_name)
74
- if current_tracker: # Ensure tracker exists before trying to reset
75
- if selected_exercise_name == "Hammer Curl": exercise_trackers[selected_exercise_name] = HammerCurl()
76
- elif selected_exercise_name == "Push Up": exercise_trackers[selected_exercise_name] = PushUp()
77
- elif selected_exercise_name == "Squat": exercise_trackers[selected_exercise_name] = Squat()
78
- # No need to call reset_reps() here as we just re-instantiated.
79
- current_set_count = 1
80
- workout_complete_message = ""
81
- workout_active = False # Changing exercise or targets stops any active workout
82
- print(f"Config changed. Workout stopped and progress reset for {selected_exercise_name}.")
83
-
84
- # Determine initial display values based on current (possibly reset) state
85
- current_reps_val = 0
86
- # Fetch the tracker again after potential re-instantiation
87
- display_tracker = exercise_trackers.get(selected_exercise_name)
88
- if display_tracker:
89
- if hasattr(display_tracker, 'counter'): # For Pushup/Squat
90
- current_reps_val = display_tracker.counter
91
- elif hasattr(display_tracker, 'counter_right'): # For Hammer Curl
92
- current_reps_val = display_tracker.counter_right
93
-
94
- reps_disp = f"{current_reps_val}/{target_reps}"
95
- if selected_exercise_name == "Hammer Curl" and display_tracker:
96
- r_c = display_tracker.counter_right
97
- l_c = display_tracker.counter_left
98
- reps_disp = f"R: {r_c}, L: {l_c} (Target: {target_reps} for R)"
99
-
100
- initial_feedback = "Select exercise, set targets, then press 'Start Workout'."
101
- if workout_active: # Should not be active after config change, but for safety
102
- initial_feedback = "Processing..."
103
- elif workout_complete_message:
104
- initial_feedback = workout_complete_message
105
-
106
- current_workout_status = workout_complete_message if workout_complete_message else ("Workout Not Active" if not workout_active else "Workout Active")
107
-
108
- return (selected_exercise_name, # For current_exercise_display
109
- reps_disp, # For reps_output
110
- f"{current_set_count}/{target_sets}", # For sets_output
111
- initial_feedback, # For feedback_output
112
- current_workout_status)# For workout_status_output
113
-
114
-
115
- def trigger_start_workout():
116
- global current_set_count, workout_complete_message, workout_active, selected_exercise_name, target_reps, target_sets
117
-
118
- print("Start Workout button clicked.")
119
- workout_active = True
120
- current_set_count = 1
121
- workout_complete_message = ""
122
-
123
- # Ensure the correct tracker is instantiated and reset for the selected exercise
124
- if selected_exercise_name == "Hammer Curl": exercise_trackers[selected_exercise_name] = HammerCurl()
125
- elif selected_exercise_name == "Push Up": exercise_trackers[selected_exercise_name] = PushUp()
126
- elif selected_exercise_name == "Squat": exercise_trackers[selected_exercise_name] = Squat()
127
- # No need to call reset_reps() on a newly instantiated object.
128
-
129
- print(f"Tracker for {selected_exercise_name} (re)initialized for start.")
130
-
131
- reps_disp = f"0/{target_reps}"
132
- if selected_exercise_name == "Hammer Curl":
133
- reps_disp = f"R: 0, L: 0 (Target: {target_reps} for R)"
134
-
135
- return (selected_exercise_name,
136
- reps_disp,
137
- f"1/{target_sets}",
138
- f"Workout Started: {selected_exercise_name}. Go!",
139
- "Workout Active")
140
-
141
- def trigger_stop_workout():
142
- global workout_active
143
- print("Stop Workout button clicked.")
144
- workout_active = False
145
- return ("Workout Stopped. Press Start to resume or change settings.", # Feedback
146
- "Workout Stopped") # Status
147
-
148
-
149
- def process_frame(video_frame_np):
150
- global selected_exercise_name, target_reps, target_sets # Read-only in this part
151
- global current_set_count, workout_complete_message, workout_active # Read/Write
152
-
153
- current_exercise_tracker = exercise_trackers.get(selected_exercise_name) # Get current tracker
154
 
155
  default_h, default_w = 480, 640
156
  if video_frame_np is not None:
@@ -159,296 +43,250 @@ def process_frame(video_frame_np):
159
  else:
160
  blank_frame = np.zeros((default_h, default_w, 3), dtype=np.uint8)
161
  cv2.putText(blank_frame, "No Camera Input", (50, default_h // 2), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 2)
162
- return blank_frame, f"0/{target_reps}", f"{current_set_count}/{target_sets}", "N/A", "No camera", "Error"
163
-
164
- # Initialize display values based on current state (might be immediately overwritten)
165
- reps_val_display = 0
166
- if current_exercise_tracker:
167
- if hasattr(current_exercise_tracker, 'counter'): reps_val_display = current_exercise_tracker.counter
168
- elif hasattr(current_exercise_tracker, 'counter_right'): reps_val_display = current_exercise_tracker.counter_right
169
-
170
- reps_display = f"{reps_val_display}/{target_reps}"
171
- if selected_exercise_name == "Hammer Curl" and current_exercise_tracker:
172
- reps_display = f"R: {current_exercise_tracker.counter_right}, L: {current_exercise_tracker.counter_left} (Target: {target_reps} for R)"
173
-
174
- sets_display = f"{current_set_count}/{target_sets}"
175
- angle_display = "N/A" # Default, will be updated if processing occurs
176
- feedback_display = "Waiting..." # Default
177
- current_workout_status = "Not Active" # Default
178
 
179
  try:
180
- if workout_active and not workout_complete_message and current_exercise_tracker:
181
- current_workout_status = "Workout Active"
182
- feedback_display = "Processing..."
183
-
184
- image_rgb = cv2.cvtColor(video_frame_np, cv2.COLOR_BGR2RGB)
185
- image_rgb.flags.writeable = False
186
- results = pose.process(image_rgb)
187
- image_rgb.flags.writeable = True
188
-
189
- if results.pose_landmarks:
190
- landmarks_mp = results.pose_landmarks.landmark
191
- frame_height, frame_width, _ = annotated_image.shape
192
- actual_reps_this_set = 0
193
-
194
- # Inner try-except for the exercise tracking and drawing logic
195
- try:
196
- if selected_exercise_name == "Hammer Curl":
197
- r_count, r_angle, l_count, l_angle, warn_r, warn_l, _, _, r_stage, l_stage = current_exercise_tracker.track_hammer_curl(landmarks_mp, annotated_image)
198
- actual_reps_this_set = r_count
199
- reps_display = f"R: {r_count}, L: {l_count} (Target: {target_reps} for R)"
200
- angle_display = f"R Ang: {int(r_angle)}, L Ang: {int(l_angle)}"
201
- feedback_list = []
202
- if warn_r: feedback_list.append(f"R: {warn_r}")
203
- if warn_l: feedback_list.append(f"L: {warn_l}")
204
- feedback_display = " | ".join(feedback_list) if feedback_list else "Good form!"
205
-
206
- elif selected_exercise_name == "Push Up":
207
- exercise_data = current_exercise_tracker.track_push_up(landmarks_mp, frame_width, frame_height)
208
- actual_reps_this_set = exercise_data.get("counter", 0)
209
- angle_display = f"L: {int(exercise_data.get('angle_left',0))}, R: {int(exercise_data.get('angle_right',0))}"
210
- feedback_display = str(exercise_data.get("feedback", "No feedback"))
211
- if 'get_drawing_annotations' in dir(current_exercise_tracker):
212
- annotations_to_draw = current_exercise_tracker.get_drawing_annotations(landmarks_mp, frame_width, frame_height, exercise_data)
213
- for ann in annotations_to_draw:
214
- if ann["type"] == "line": cv2.line(annotated_image, tuple(ann["start_point"]), tuple(ann["end_point"]), ann["color_bgr"], ann["thickness"])
215
- elif ann["type"] == "circle": cv2.circle(annotated_image, tuple(ann["center_point"]), ann["radius"], ann["color_bgr"], -1 if ann.get("filled", False) else ann["thickness"])
216
- elif ann["type"] == "text": cv2.putText(annotated_image, ann["text_content"], tuple(ann["position"]), cv2.FONT_HERSHEY_SIMPLEX, ann["font_scale"], ann["color_bgr"], ann["thickness"])
217
-
218
- elif selected_exercise_name == "Squat":
219
- exercise_data = current_exercise_tracker.track_squat(landmarks_mp, frame_width, frame_height)
220
- actual_reps_this_set = exercise_data.get("counter", 0)
221
- angle_display = f"L: {int(exercise_data.get('angle_left',0))}, R: {int(exercise_data.get('angle_right',0))}"
222
- feedback_display = str(exercise_data.get("feedback", "No feedback"))
223
- if 'get_drawing_annotations' in dir(current_exercise_tracker):
224
- annotations_to_draw = current_exercise_tracker.get_drawing_annotations(landmarks_mp, frame_width, frame_height, exercise_data)
225
- for ann in annotations_to_draw:
226
- if ann["type"] == "line": cv2.line(annotated_image, tuple(ann["start_point"]), tuple(ann["end_point"]), ann["color_bgr"], ann["thickness"])
227
- elif ann["type"] == "circle": cv2.circle(annotated_image, tuple(ann["center_point"]), ann["radius"], ann["color_bgr"], -1 if ann.get("filled", False) else ann["thickness"])
228
- elif ann["type"] == "text": cv2.putText(annotated_image, ann["text_content"], tuple(ann["position"]), cv2.FONT_HERSHEY_SIMPLEX, ann["font_scale"], ann["color_bgr"], ann["thickness"])
229
-
230
- if selected_exercise_name != "Hammer Curl": # Update general reps display
231
- reps_display = f"{actual_reps_this_set}/{target_reps}"
232
-
233
- # Set completion logic
234
- if actual_reps_this_set >= target_reps:
235
- if current_set_count < target_sets:
236
- current_set_count += 1
237
- current_exercise_tracker.reset_reps() # Reset reps for the next set
238
- feedback_display = f"Set {current_set_count-1} complete! Starting set {current_set_count}."
239
- if selected_exercise_name == "Hammer Curl": reps_display = f"R: 0, L: 0 (Target: {target_reps} for R)"
240
- else: reps_display = f"0/{target_reps}"
241
- elif current_set_count >= target_sets: # Reached target sets
242
- feedback_display = "Workout Complete!"
243
- workout_complete_message = "Workout Complete!"
244
- workout_active = False # Stop workout automatically
245
- if selected_exercise_name == "Hammer Curl": reps_display = f"R: {target_reps}, L: {target_reps} (Target: {target_reps} for R)"
246
- else: reps_display = f"{target_reps}/{target_reps}"
247
- current_workout_status = workout_complete_message if workout_complete_message else "Workout Active"
 
 
 
 
 
 
 
 
 
 
 
 
 
248
 
249
- except Exception as e_exercise:
250
- print(f"PROCESS_FRAME: Error during exercise '{selected_exercise_name}' logic: {e_exercise}")
251
- print(traceback.format_exc())
252
- cv2.putText(annotated_image, f"Error in {selected_exercise_name}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
253
- feedback_display = f"Error in {selected_exercise_name} processing."
254
-
255
- else: # No landmarks detected, but workout is active
256
- feedback_display = "No person detected. Adjust position."
257
- # Draw generic landmarks if pose was processed (results will be non-None but results.pose_landmarks is None)
258
- # This case is subtle; if results is not None but results.pose_landmarks is, mp_drawing might error.
259
- # For simplicity, just show "No person detected" on image.
260
- cv2.putText(annotated_image, "No person detected", (50, default_h // 2), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255),2)
261
-
262
- # This elif is for when workout_complete_message IS set
 
 
 
 
 
 
 
 
 
263
  elif workout_complete_message:
264
  feedback_display = workout_complete_message
265
- current_workout_status = workout_complete_message
266
  reps_display = f"{target_reps}/{target_reps}" if selected_exercise_name != "Hammer Curl" else f"R: {target_reps}, L: {target_reps} (Target: {target_reps} for R)"
267
- sets_display = f"{target_sets}/{target_sets}" # Show completed sets
268
- # Optionally, draw last known pose or a static image
269
- if results and results.pose_landmarks: # results might be from a previous frame if workout_active just became false
270
  mp_drawing.draw_landmarks(annotated_image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style())
271
-
272
- # This elif is for when workout is NOT active (and not complete)
273
- elif not workout_active:
274
- feedback_display = "Workout stopped or not started. Press 'Start Workout'."
275
- current_workout_status = "Workout Stopped / Not Started"
276
- # Draw generic landmarks if pose was processed (results might be available)
277
  if results and results.pose_landmarks :
278
  mp_drawing.draw_landmarks(annotated_image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style())
279
- else: # If no results at all (e.g. initial state before any processing)
280
- cv2.putText(annotated_image, "Ready to Start", (50, default_h // 2), cv2.FONT_HERSHEY_SIMPLEX, 1, (100,100,100),2)
281
-
 
282
 
283
- # Final check on annotated_image before returning
284
  if not isinstance(annotated_image, np.ndarray) or annotated_image.ndim != 3 or annotated_image.shape[2] != 3:
285
- print(f"PROCESS_FRAME: Error - annotated_image is invalid before return. Reverting to blank.")
286
- annotated_image = np.zeros((default_h, default_w, 3), dtype=np.uint8) # Fallback
287
  cv2.putText(annotated_image, "Display Error", (50, default_h // 2), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255),2)
288
-
289
- return annotated_image, reps_display, sets_display, angle_display, feedback_display, current_workout_status
290
 
291
  except Exception as e_main:
292
  print(f"PROCESS_FRAME: CRITICAL error in process_frame: {e_main}")
293
  print(traceback.format_exc())
294
  error_frame = np.zeros((default_h, default_w, 3), dtype=np.uint8)
295
- cv2.putText(error_frame, "Critical Error", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
296
- # Ensure all 6 output values are returned
297
- return error_frame, "Error", "Error", "Error", "Critical Error", "Error"
298
 
299
-
300
- # --- Custom CSS ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  custom_css = """
302
- body, .gradio-container {
303
- background: linear-gradient(to bottom right, #2A002A, #5D3FD3) !important; /* Darker Violet to a brighter Violet */
304
- color: #E0E0E0 !important;
 
 
305
  }
306
- .gradio-container { font-family: 'Exo 2', sans-serif !important; }
307
  label, .gr-checkbox-label span { /* Target labels and checkbox labels */
308
- color: #D0D0D0 !important;
309
  font-weight: bold !important;
310
  }
311
- /* Titles for sections */
312
- .gradio-group > .gr-box > .gr-markdown > div > h3, /* For gr.Markdown("### Title") inside gr.Group */
313
- .gr-form > .gr-markdown > div > h3 { /* For gr.Markdown("### Title") as direct child of form/column */
314
- color: #FFFFFF !important;
315
- text-align: left !important; /* Titles in sections are usually left-aligned */
316
- font-family: 'Exo 2', sans-serif !important;
317
- margin-bottom: 0.5em !important; /* Add some space below section titles */
318
- }
319
- /* Main Page Title */
320
- .gradio-container > .main > .wrap > .contain > . ΟŒΟ€ΞΏΟ… > .output > .gr-markdown > div > h1, /* More specific for main H1 */
321
- .gradio-container > div > .gr-markdown > div > h1 { /* Fallback for main H1 */
322
  color: #FFFFFF !important;
323
  text-align: center !important;
324
- font-family: 'Exo 2', sans-serif !important;
325
- font-size: 2.5em !important; /* Larger main title */
326
- margin-top: 0.5em !important;
327
- margin-bottom: 0.2em !important;
328
- }
329
- /* Subtitle Markdown */
330
- .gradio-container > .main > .wrap > .contain > . ΟŒΟ€ΞΏΟ… > .output > .gr-markdown:nth-of-type(2) > div > p,
331
- .gradio-container > div > .gr-markdown:nth-of-type(2) > div > p {
332
- color: #E8E8E8 !important;
333
- text-align: center !important;
334
- font-size: 1.1em !important;
335
- margin-bottom: 1.5em !important;
336
  }
337
- .gr-button { /* General button styling */
338
- font-family: 'Exo 2', sans-serif !important;
339
- border-radius: 8px !important;
340
- font-weight: bold !important;
341
  }
342
- .gradio-group { /* Styling for gr.Group to look like cards */
343
- background-color: rgba(0,0,0,0.15) !important;
344
- border-radius: 10px !important;
345
- border: 1px solid rgba(255,255,255,0.1) !important;
346
- padding: 15px !important;
347
- margin-bottom: 15px !important;
348
  }
349
- .gradio-image { /* Style for the image component (webcam feed) */
350
- border-radius: 10px !important;
351
- overflow: hidden; /* Ensures content respects border-radius */
 
352
  }
353
  """
354
 
355
  # --- Gradio Theme ---
 
 
356
  theme = gr.themes.Base(
357
  font=[gr.themes.GoogleFont("Exo 2"), "ui-sans-serif", "system-ui", "sans-serif"],
358
- primary_hue=gr.themes.colors.purple,
359
- secondary_hue=gr.themes.colors.pink,
360
- neutral_hue=gr.themes.colors.slate
361
  ).set(
362
- body_text_color="#E0E0E0", # General text color, should influence input text too
363
- input_background_fill="rgba(255,255,255,0.05)",
364
- input_border_color="rgba(255,255,255,0.2)",
365
- # input_text_color="#FFFFFF", # <--- REMOVED THIS LINE
366
- button_primary_background_fill=gr.themes.colors.purple,
367
- button_primary_background_fill_hover=gr.themes.colors.pink,
368
- button_primary_text_color="#FFFFFF",
369
- button_secondary_background_fill=gr.themes.colors.pink,
370
- button_secondary_background_fill_hover=gr.themes.colors.purple,
371
- button_secondary_text_color="#FFFFFF",
372
- block_title_text_color = "#FFFFFF",
373
- block_label_text_color = "#E0E0E0",
374
- border_color_accent = gr.themes.colors.purple,
375
- background_fill_primary = "#1E001E",
376
- background_fill_secondary = "#2A0A2A",
377
  )
 
 
378
  # --- Gradio Interface ---
379
- exercise_choices_list = ["Hammer Curl", "Push Up", "Squat"]
380
 
 
381
  with gr.Blocks(theme=theme, css=custom_css) as iface:
382
- gr.Markdown("# LIVE TRAINING SESSION")
383
- gr.Markdown("AI-powered exercise tracking and feedback")
384
 
385
- with gr.Row(equal_height=False):
386
- with gr.Column(scale=2):
387
- webcam_input = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="Live Workout Feed")
388
-
389
  with gr.Column(scale=1):
390
- with gr.Group():
391
- gr.Markdown("### Select Exercise")
392
- with gr.Row(equal_height=True): # Make buttons same height
393
- hc_btn = gr.Button("Hammer Curl", scale=1)
394
- pu_btn = gr.Button("Push Up", scale=1)
395
- sq_btn = gr.Button("Squat", scale=1)
396
 
397
- with gr.Group():
398
- gr.Markdown("### Configure Workout")
399
- with gr.Row():
400
- target_sets_number = gr.Number(value=target_sets, label="Sets:", precision=0, minimum=1, scale=1)
401
- target_reps_number = gr.Number(value=target_reps, label="Reps:", precision=0, minimum=1, scale=1)
402
- with gr.Row():
403
- start_button = gr.Button("Start Workout", variant="primary", scale=1)
404
- stop_button = gr.Button("Stop Workout", variant="secondary", scale=1) # Using secondary for stop
405
-
406
- with gr.Group():
407
- gr.Markdown("### Current Status")
408
- current_exercise_display = gr.Textbox(label="Exercise:", value=selected_exercise_name, interactive=False)
409
- sets_output = gr.Textbox(label="Set:", interactive=False)
410
- reps_output = gr.Textbox(label="Repetitions:", interactive=False)
411
- angle_output = gr.Textbox(label="Angle Details:", interactive=False) # <-- Add this line back
412
- feedback_output = gr.Textbox(label="Feedback:", lines=3, max_lines=5, interactive=False)
413
- workout_status_output = gr.Textbox(label="Workout Status:", interactive=False)
414
-
415
- # Define outputs that are updated by multiple control changes (exercise selection, target changes)
416
- # These are: current exercise display, reps, sets, feedback, workout status
417
- shared_ui_outputs = [current_exercise_display, reps_output, sets_output, feedback_output, workout_status_output]
418
-
419
- # Outputs from the main process_frame (includes video + all text fields for status)
420
  process_frame_outputs = [webcam_input, reps_output, sets_output, angle_output, feedback_output, workout_status_output]
421
 
 
 
 
 
422
 
423
- def create_config_change_handler(exercise_name_to_select):
424
- # This closure captures the exercise_name_to_select for button clicks
425
- def handler(reps, sets):
426
- # Call the main update function
427
- return update_targets_and_exercise_display(exercise_name_to_select, reps, sets)
428
- return handler
429
-
430
- # Exercise selection buttons
431
- hc_btn.click(create_config_change_handler("Hammer Curl"), inputs=[target_reps_number, target_sets_number], outputs=shared_ui_outputs)
432
- pu_btn.click(create_config_change_handler("Push Up"), inputs=[target_reps_number, target_sets_number], outputs=shared_ui_outputs)
433
- sq_btn.click(create_config_change_handler("Squat"), inputs=[target_reps_number, target_sets_number], outputs=shared_ui_outputs)
434
-
435
- # Target number changes also call update_targets_and_exercise_display
436
- # They need to know the currently selected exercise to pass it correctly
437
- # For this, we'll pass selected_exercise_name (which is global and updated by buttons)
438
- # The target_reps_number and target_sets_number inputs will be their own current values
439
- target_reps_number.change(lambda r, s: update_targets_and_exercise_display(selected_exercise_name, r, s),
440
- inputs=[target_reps_number, target_sets_number],
441
- outputs=shared_ui_outputs)
442
- target_sets_number.change(lambda r, s: update_targets_and_exercise_display(selected_exercise_name, r, s),
443
- inputs=[target_reps_number, target_sets_number],
444
- outputs=shared_ui_outputs)
445
-
446
- # Start and Stop buttons
447
- start_button.click(trigger_start_workout, inputs=None, outputs=shared_ui_outputs)
448
- stop_button.click(trigger_stop_workout, inputs=None, outputs=[feedback_output, workout_status_output])
449
-
450
- # Video stream processing
451
- webcam_input.stream(fn=process_frame, inputs=[webcam_input], outputs=process_frame_outputs)
452
 
453
  if __name__ == "__main__":
454
- iface.launch(debug=False, share=False)
 
3
  import numpy as np
4
  import mediapipe as mp
5
  import time
6
+ import traceback # For detailed error logging
7
 
8
  # Import your exercise classes
9
  from exercises.hammer_curl import HammerCurl
 
22
  "Push Up": PushUp(),
23
  "Squat": Squat()
24
  }
25
+ current_exercise_tracker = None
 
26
  selected_exercise_name = "Hammer Curl" # Default exercise
27
 
28
+ # Target and progress tracking
29
+ target_reps = 10 # Default target reps
30
+ target_sets = 3 # Default target sets
31
  current_set_count = 1
32
  workout_complete_message = ""
 
33
 
34
 
35
+ def process_frame(video_frame_np, exercise_name_choice, target_reps_input, target_sets_input):
36
+ global current_exercise_tracker, selected_exercise_name
37
+ global target_reps, target_sets, current_set_count, workout_complete_message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
  default_h, default_w = 480, 640
40
  if video_frame_np is not None:
 
43
  else:
44
  blank_frame = np.zeros((default_h, default_w, 3), dtype=np.uint8)
45
  cv2.putText(blank_frame, "No Camera Input", (50, default_h // 2), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255), 2)
46
+ return blank_frame, f"0/{target_reps}", f"{current_set_count}/{target_sets}", "No frame", "No camera", "Error: No Frame"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  try:
49
+ new_target_reps_val = target_reps
50
+ try:
51
+ new_target_reps_val = int(target_reps_input)
52
+ if new_target_reps_val > 0 and target_reps != new_target_reps_val:
53
+ target_reps = new_target_reps_val
54
+ if current_exercise_tracker: current_exercise_tracker.reset_reps()
55
+ current_set_count = 1
56
+ workout_complete_message = ""
57
+ except ValueError: pass
58
+
59
+ new_target_sets_val = target_sets
60
+ try:
61
+ new_target_sets_val = int(target_sets_input)
62
+ if new_target_sets_val > 0 and target_sets != new_target_sets_val:
63
+ target_sets = new_target_sets_val
64
+ if current_exercise_tracker: current_exercise_tracker.reset_reps()
65
+ current_set_count = 1
66
+ workout_complete_message = ""
67
+ except ValueError: pass
68
+
69
+ if selected_exercise_name != exercise_name_choice:
70
+ selected_exercise_name = exercise_name_choice
71
+ if selected_exercise_name in exercise_trackers:
72
+ if selected_exercise_name == "Hammer Curl": exercise_trackers[selected_exercise_name] = HammerCurl()
73
+ elif selected_exercise_name == "Push Up": exercise_trackers[selected_exercise_name] = PushUp()
74
+ elif selected_exercise_name == "Squat": exercise_trackers[selected_exercise_name] = Squat()
75
+ current_set_count = 1
76
+ workout_complete_message = ""
77
+ else: current_exercise_tracker = None
78
+ current_exercise_tracker = exercise_trackers.get(selected_exercise_name)
79
+
80
+ image_rgb = cv2.cvtColor(video_frame_np, cv2.COLOR_BGR2RGB)
81
+ image_rgb.flags.writeable = False
82
+ results = pose.process(image_rgb)
83
+ image_rgb.flags.writeable = True
84
+
85
+ reps_display = f"0/{target_reps}"
86
+ sets_display = f"{current_set_count}/{target_sets}"
87
+ angle_display = "N/A"
88
+ feedback_display = "Initializing..."
89
+ temp_workout_message = workout_complete_message
90
+
91
+ if results.pose_landmarks and current_exercise_tracker and not workout_complete_message:
92
+ landmarks_mp = results.pose_landmarks.landmark
93
+ frame_height, frame_width, _ = annotated_image.shape
94
+ actual_reps_this_set = 0
95
+
96
+ try:
97
+ if selected_exercise_name == "Hammer Curl":
98
+ r_count, r_angle, l_count, l_angle, warn_r, warn_l, _, _, r_stage, l_stage = current_exercise_tracker.track_hammer_curl(landmarks_mp, annotated_image)
99
+ actual_reps_this_set = r_count
100
+ reps_display = f"R: {r_count}, L: {l_count} (Target: {target_reps} for R)"
101
+ angle_display = f"R Ang: {int(r_angle)}, L Ang: {int(l_angle)}"
102
+ feedback_list = []
103
+ if warn_r: feedback_list.append(f"R: {warn_r}")
104
+ if warn_l: feedback_list.append(f"L: {warn_l}")
105
+ feedback_display = " | ".join(feedback_list) if feedback_list else "Good form!"
106
+
107
+ elif selected_exercise_name == "Push Up":
108
+ exercise_data = current_exercise_tracker.track_push_up(landmarks_mp, frame_width, frame_height)
109
+ actual_reps_this_set = exercise_data.get("counter", 0)
110
+ angle_display = f"L: {int(exercise_data.get('angle_left',0))}, R: {int(exercise_data.get('angle_right',0))}"
111
+ feedback_display = str(exercise_data.get("feedback", "No feedback"))
112
+ if 'get_drawing_annotations' in dir(current_exercise_tracker):
113
+ annotations_to_draw = current_exercise_tracker.get_drawing_annotations(landmarks_mp, frame_width, frame_height, exercise_data)
114
+ for ann in annotations_to_draw:
115
+ if ann["type"] == "line": cv2.line(annotated_image, tuple(ann["start_point"]), tuple(ann["end_point"]), ann["color_bgr"], ann["thickness"])
116
+ elif ann["type"] == "circle": cv2.circle(annotated_image, tuple(ann["center_point"]), ann["radius"], ann["color_bgr"], -1 if ann.get("filled", False) else ann["thickness"])
117
+ elif ann["type"] == "text": cv2.putText(annotated_image, ann["text_content"], tuple(ann["position"]), cv2.FONT_HERSHEY_SIMPLEX, ann["font_scale"], ann["color_bgr"], ann["thickness"])
118
+
119
+ elif selected_exercise_name == "Squat":
120
+ exercise_data = current_exercise_tracker.track_squat(landmarks_mp, frame_width, frame_height)
121
+ actual_reps_this_set = exercise_data.get("counter", 0)
122
+ angle_display = f"L: {int(exercise_data.get('angle_left',0))}, R: {int(exercise_data.get('angle_right',0))}"
123
+ feedback_display = str(exercise_data.get("feedback", "No feedback"))
124
+ if 'get_drawing_annotations' in dir(current_exercise_tracker):
125
+ annotations_to_draw = current_exercise_tracker.get_drawing_annotations(landmarks_mp, frame_width, frame_height, exercise_data)
126
+ for ann in annotations_to_draw:
127
+ if ann["type"] == "line": cv2.line(annotated_image, tuple(ann["start_point"]), tuple(ann["end_point"]), ann["color_bgr"], ann["thickness"])
128
+ elif ann["type"] == "circle": cv2.circle(annotated_image, tuple(ann["center_point"]), ann["radius"], ann["color_bgr"], -1 if ann.get("filled", False) else ann["thickness"])
129
+ elif ann["type"] == "text": cv2.putText(annotated_image, ann["text_content"], tuple(ann["position"]), cv2.FONT_HERSHEY_SIMPLEX, ann["font_scale"], ann["color_bgr"], ann["thickness"])
130
 
131
+ if selected_exercise_name != "Hammer Curl":
132
+ reps_display = f"{actual_reps_this_set}/{target_reps}"
133
+
134
+ if actual_reps_this_set >= target_reps:
135
+ if current_set_count < target_sets:
136
+ current_set_count += 1
137
+ current_exercise_tracker.reset_reps()
138
+ feedback_display = f"Set {current_set_count-1} complete! Starting set {current_set_count}."
139
+ if selected_exercise_name == "Hammer Curl": reps_display = f"R: 0, L: 0 (Target: {target_reps} for R)"
140
+ else: reps_display = f"0/{target_reps}"
141
+ elif current_set_count >= target_sets:
142
+ feedback_display = "Workout Complete!"
143
+ workout_complete_message = "Workout Complete! Change targets or exercise to restart."
144
+ if selected_exercise_name == "Hammer Curl": reps_display = f"R: {target_reps}, L: {target_reps} (Target: {target_reps} for R)"
145
+ else: reps_display = f"{target_reps}/{target_reps}"
146
+ temp_workout_message = workout_complete_message
147
+
148
+ except Exception as e_exercise:
149
+ print(f"PROCESS_FRAME: Error during exercise '{selected_exercise_name}' logic: {e_exercise}")
150
+ print(traceback.format_exc())
151
+ cv2.putText(annotated_image, f"Error in {selected_exercise_name}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
152
+ feedback_display = f"Error in {selected_exercise_name} processing."
153
+
154
  elif workout_complete_message:
155
  feedback_display = workout_complete_message
 
156
  reps_display = f"{target_reps}/{target_reps}" if selected_exercise_name != "Hammer Curl" else f"R: {target_reps}, L: {target_reps} (Target: {target_reps} for R)"
157
+ sets_display = f"{target_sets}/{target_sets}"
158
+ if results.pose_landmarks:
 
159
  mp_drawing.draw_landmarks(annotated_image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style())
160
+
161
+ else:
162
+ feedback_display = "No person detected or exercise not selected properly."
 
 
 
163
  if results and results.pose_landmarks :
164
  mp_drawing.draw_landmarks(annotated_image, results.pose_landmarks, mp_pose.POSE_CONNECTIONS, landmark_drawing_spec=mp_drawing_styles.get_default_pose_landmarks_style())
165
+ else:
166
+ cv2.putText(annotated_image, "No person detected", (50, default_h // 2), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255),2)
167
+
168
+ sets_display = f"{current_set_count}/{target_sets}"
169
 
 
170
  if not isinstance(annotated_image, np.ndarray) or annotated_image.ndim != 3 or annotated_image.shape[2] != 3:
171
+ annotated_image = np.zeros((default_h, default_w, 3), dtype=np.uint8)
 
172
  cv2.putText(annotated_image, "Display Error", (50, default_h // 2), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,255),2)
173
+
174
+ return annotated_image, reps_display, sets_display, angle_display, feedback_display, temp_workout_message
175
 
176
  except Exception as e_main:
177
  print(f"PROCESS_FRAME: CRITICAL error in process_frame: {e_main}")
178
  print(traceback.format_exc())
179
  error_frame = np.zeros((default_h, default_w, 3), dtype=np.uint8)
180
+ cv2.putText(error_frame, f"Error: {e_main}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
181
+ return error_frame, "Error", "Error", "Error", "Critical Error", "Critical Error"
 
182
 
183
+ def trigger_reset_workout():
184
+ global current_set_count, workout_complete_message, selected_exercise_name, target_reps, target_sets, current_exercise_tracker
185
+
186
+ current_set_count = 1
187
+ workout_complete_message = ""
188
+
189
+ if current_exercise_tracker:
190
+ current_exercise_tracker.reset_reps()
191
+
192
+ reset_reps_display = f"0/{target_reps}"
193
+ if selected_exercise_name == "Hammer Curl":
194
+ reset_reps_display = f"R: 0, L: 0 (Target: {target_reps} for R)"
195
+
196
+ reset_sets_display = f"1/{target_sets}"
197
+ reset_angle_display = "N/A"
198
+ reset_feedback_display = "Workout Reset. Ready to start."
199
+ reset_workout_status = ""
200
+
201
+ return reset_reps_display, reset_sets_display, reset_angle_display, reset_feedback_display, reset_workout_status
202
+
203
+ # --- Custom CSS for gradient background and styling ---
204
+ # Note: Applying gradient to the root/body might be overridden by Gradio's specific block styling.
205
+ # It's often better to target .gradio-container or specific blocks if possible.
206
+ # However, for broad effect, body is a start. More specific selectors might be needed for full coverage.
207
+ # --- Custom CSS for gradient background and styling ---
208
  custom_css = """
209
+ body {
210
+ background: linear-gradient(to bottom right, #301934, #8A2BE0) !important; /* Dark Violet to a brighter Violet */
211
+ }
212
+ .gradio-container {
213
+ background: linear-gradient(to bottom right, #301934, #8A2BE0) !important; /* Ensure container also gets it */
214
  }
 
215
  label, .gr-checkbox-label span { /* Target labels and checkbox labels */
216
+ color: #E8E8E8 !important; /* Slightly brighter light gray for labels */
217
  font-weight: bold !important;
218
  }
219
+ h1 {
 
 
 
 
 
 
 
 
 
 
220
  color: #FFFFFF !important;
221
  text-align: center !important;
 
 
 
 
 
 
 
 
 
 
 
 
222
  }
223
+ .prose { /* Markdown text */
224
+ color: #F0F0F0 !important;
 
 
225
  }
226
+ /* General text within blocks that isn't a label or title */
227
+ .gr-block .gr-box > div > p, .gr-block .gr-box > div > span {
228
+ color: #E0E0E0 !important;
 
 
 
229
  }
230
+ /* You might need to adjust button font styling with more specific selectors if this doesn't work */
231
+ button.gr-button-primary {
232
+ font-family: 'Exo 2', sans-serif !important;
233
+ /* The theme's primary_hue should handle button background and text color for contrast */
234
  }
235
  """
236
 
237
  # --- Gradio Theme ---
238
+ # Using a base theme to set font and then overriding some colors.
239
+ # For button color, if primary_hue doesn't give desired button color, specific CSS might be needed.
240
  theme = gr.themes.Base(
241
  font=[gr.themes.GoogleFont("Exo 2"), "ui-sans-serif", "system-ui", "sans-serif"],
242
+ primary_hue=gr.themes.colors.amber, # For buttons - gives a yellowish/golden hue
243
+ secondary_hue=gr.themes.colors.blue,
244
+ neutral_hue=gr.themes.colors.gray
245
  ).set(
246
+ body_text_color="#E0E0E0", # Light gray for general text (this should also affect input text)
247
+ input_background_fill="#4A2A6C", # Darker violet for input backgrounds
248
+ input_border_color="#6A3AA2",
249
+ # button_primary_text_color="#111111", # Often better to let theme handle this or use CSS
250
+ # Ensure other text elements have good contrast automatically or via custom_css
 
 
 
 
 
 
 
 
 
 
251
  )
252
+
253
+
254
  # --- Gradio Interface ---
255
+ exercise_choices = ["Hammer Curl", "Push Up", "Squat"]
256
 
257
+ # Pass the theme and custom_css to gr.Blocks
258
  with gr.Blocks(theme=theme, css=custom_css) as iface:
259
+ gr.Markdown("# Live AI Trainer") # This will be styled by H1 in CSS
260
+ gr.Markdown("Select an exercise, set your targets, and get real-time feedback on your form and reps.") # Styled by .prose
261
 
262
+ with gr.Row():
263
+ with gr.Column(scale=2):
264
+ webcam_input = gr.Image(sources=["webcam"], streaming=True, type="numpy", label="Your Webcam")
 
265
  with gr.Column(scale=1):
266
+ gr.Markdown("### Controls") # Markdown default color should be handled by theme or prose
267
+ exercise_dropdown = gr.Dropdown(choices=exercise_choices, label="Select Exercise", value="Hammer Curl")
268
+ target_reps_number = gr.Number(value=target_reps, label="Target Reps per Set", precision=0, minimum=1)
269
+ target_sets_number = gr.Number(value=target_sets, label="Target Sets", precision=0, minimum=1)
 
 
270
 
271
+ reset_button = gr.Button("Reset Workout") # Will use primary_hue from theme
272
+
273
+ gr.Markdown("### Progress")
274
+ reps_output = gr.Textbox(label="Reps Progress")
275
+ sets_output = gr.Textbox(label="Sets Progress")
276
+ angle_output = gr.Textbox(label="Angle Details")
277
+ feedback_output = gr.Textbox(label="Feedback", lines=3, max_lines=5)
278
+ workout_status_output = gr.Textbox(label="Workout Status", lines=2, interactive=False)
279
+
280
+ process_frame_inputs = [webcam_input, exercise_dropdown, target_reps_number, target_sets_number]
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  process_frame_outputs = [webcam_input, reps_output, sets_output, angle_output, feedback_output, workout_status_output]
282
 
283
+ webcam_input.stream(fn=process_frame, inputs=process_frame_inputs, outputs=process_frame_outputs)
284
+ exercise_dropdown.change(fn=process_frame, inputs=process_frame_inputs, outputs=process_frame_outputs)
285
+ target_reps_number.change(fn=process_frame, inputs=process_frame_inputs, outputs=process_frame_outputs)
286
+ target_sets_number.change(fn=process_frame, inputs=process_frame_inputs, outputs=process_frame_outputs)
287
 
288
+ reset_button_outputs = [reps_output, sets_output, angle_output, feedback_output, workout_status_output]
289
+ reset_button.click(fn=trigger_reset_workout, inputs=None, outputs=reset_button_outputs)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
  if __name__ == "__main__":
292
+ iface.launch(debug=False, share=False) # share=False is default but good to be explicit for Spaces