zforkash commited on
Commit
8afa4d1
·
verified ·
1 Parent(s): 6dffeff
Files changed (1) hide show
  1. app.py +260 -189
app.py CHANGED
@@ -1,99 +1,134 @@
1
  import streamlit as st
2
- import cv2
3
- import mediapipe as mp
4
- import numpy as np
5
- from streamlit_webrtc import webrtc_streamer, VideoTransformerBase
6
  import av
 
 
 
 
 
 
7
  import threading
8
- from dataclasses import dataclass
9
- from typing import List
10
 
11
- # Mediapipe setup
12
- mp_drawing = mp.solutions.drawing_utils
 
 
 
 
 
 
 
 
13
  mp_pose = mp.solutions.pose
 
14
 
15
- # Custom CSS
 
 
 
 
 
 
 
 
16
  st.markdown("""
17
  <style>
18
  .main {
19
- background: linear-gradient(135deg, #001f3f 0%, #00b4d8 100%);
20
  }
21
  .stButton > button {
22
- background-color: #00b4d8;
 
23
  color: white;
24
- border: none;
25
- padding: 0.5rem 2rem;
26
  border-radius: 5px;
27
- margin: 0.5rem;
28
- transition: all 0.3s;
29
- }
30
- .stButton > button:hover {
31
- background-color: #0077b6;
32
- }
33
- h1, h2, h3 {
34
- color: #001f3f;
35
  }
36
  .workout-container {
37
- background: rgba(0, 180, 216, 0.1);
38
- padding: 2rem;
39
  border-radius: 10px;
40
- margin: 1rem 0;
41
  }
42
- .feedback-text {
43
- background: rgba(0, 31, 63, 0.1);
44
- padding: 1rem;
45
- border-radius: 5px;
46
- margin: 1rem 0;
47
  }
48
  </style>
49
  """, unsafe_allow_html=True)
50
 
51
- @dataclass
52
- class ExerciseState:
53
- counter: int = 0
54
- stage: str = None
55
- feedback: str = ""
56
-
57
- # Global state
58
- state = ExerciseState()
59
- lock = threading.Lock()
60
-
61
- def calculate_angle(a, b, c):
62
- """Calculate angle between three points."""
63
- a = np.array(a)
64
- b = np.array(b)
65
- c = np.array(c)
66
-
67
- radians = np.arctan2(c[1]-b[1], c[0]-b[0]) - np.arctan2(a[1]-b[1], a[0]-b[0])
68
- angle = np.abs(np.degrees(radians))
69
-
70
- if angle > 180.0:
71
- angle = 360 - angle
72
- return angle
73
-
74
- def calculate_lateral_raise_angle(shoulder, wrist):
75
- """Calculate angle for lateral raise."""
76
- horizontal_reference = np.array([1, 0])
77
- arm_vector = np.array([wrist[0] - shoulder[0], wrist[1] - shoulder[1]])
78
-
79
- dot_product = np.dot(horizontal_reference, arm_vector)
80
- magnitude_reference = np.linalg.norm(horizontal_reference)
81
- magnitude_arm = np.linalg.norm(arm_vector)
82
-
83
- if magnitude_arm == 0 or magnitude_reference == 0:
84
- return 0
85
-
86
- cos_angle = dot_product / (magnitude_reference * magnitude_arm)
87
- angle = np.arccos(np.clip(cos_angle, -1.0, 1.0))
88
- return np.degrees(angle)
89
-
90
- class VideoTransformer(VideoTransformerBase):
91
  def __init__(self):
92
- self.pose = mp_pose.Pose(min_detection_confidence=0.5, min_tracking_confidence=0.5)
93
- self.workout_type = "bicep_curl" # Default workout
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
- def process_bicep_curl(self, landmarks):
96
- """Process frame for bicep curl exercise."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,
98
  landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y]
99
  elbow = [landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x,
@@ -101,41 +136,59 @@ class VideoTransformer(VideoTransformerBase):
101
  wrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,
102
  landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y]
103
 
104
- angle = calculate_angle(shoulder, elbow, wrist)
105
-
106
- with lock:
107
- if angle > 160 and state.stage != "down":
108
- state.stage = "down"
109
- state.feedback = "Lower the weight"
110
- elif angle < 40 and state.stage == "down":
111
- state.stage = "up"
112
- state.counter += 1
113
- state.feedback = f"Good rep! Count: {state.counter}"
114
 
115
- return angle
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
- def process_lateral_raise(self, landmarks):
118
- """Process frame for lateral raise exercise."""
119
  shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,
120
  landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y]
 
 
121
  wrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,
122
  landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y]
123
 
124
- angle = calculate_lateral_raise_angle(shoulder, wrist)
125
 
126
- with lock:
127
- if angle < 20 and state.stage != "down":
128
- state.stage = "down"
129
- state.feedback = "Raise your arms"
130
- elif 70 <= angle <= 110 and state.stage == "down":
131
- state.stage = "up"
132
- state.counter += 1
133
- state.feedback = f"Good rep! Count: {state.counter}"
 
 
134
 
135
- return angle
 
 
 
 
 
136
 
137
- def process_shoulder_press(self, landmarks):
138
- """Process frame for shoulder press exercise."""
139
  shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,
140
  landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y]
141
  elbow = [landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x,
@@ -143,118 +196,136 @@ class VideoTransformer(VideoTransformerBase):
143
  wrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,
144
  landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y]
145
 
146
- angle = calculate_angle(shoulder, elbow, wrist)
147
 
148
- with lock:
149
- if 80 <= angle <= 100 and state.stage != "down":
150
- state.stage = "down"
151
- state.feedback = "Press up!"
152
- elif angle > 160 and state.stage == "down":
153
- state.stage = "up"
154
- state.counter += 1
155
- state.feedback = f"Good rep! Count: {state.counter}"
 
 
156
 
157
- return angle
 
 
 
 
 
158
 
159
- def recv(self, frame):
160
- img = frame.to_ndarray(format="bgr24")
 
 
 
 
 
 
161
 
162
- # Process the image
163
- image = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
164
- results = self.pose.process(image)
165
 
166
- if results.pose_landmarks:
167
- # Draw pose landmarks
168
- mp_drawing.draw_landmarks(
169
- img, results.pose_landmarks, mp_pose.POSE_CONNECTIONS,
170
- mp_drawing.DrawingSpec(color=(0, 255, 0), thickness=2, circle_radius=2),
171
- mp_drawing.DrawingSpec(color=(0, 255, 0), thickness=2)
172
- )
173
 
174
- # Process based on workout type
175
- if self.workout_type == "bicep_curl":
176
- angle = self.process_bicep_curl(results.pose_landmarks.landmark)
177
- elif self.workout_type == "lateral_raise":
178
- angle = self.process_lateral_raise(results.pose_landmarks.landmark)
179
- else: # shoulder_press
180
- angle = self.process_shoulder_press(results.pose_landmarks.landmark)
181
 
182
- # Draw angle and counter
183
- cv2.putText(img, f"Angle: {angle:.2f}", (10, 30),
184
- cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
185
- cv2.putText(img, f"Counter: {state.counter}", (10, 70),
186
- cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
187
- cv2.putText(img, f"Feedback: {state.feedback}", (10, 110),
188
- cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
189
-
190
- return av.VideoFrame.from_ndarray(img, format="bgr24")
191
 
192
  def main():
 
193
  st.title("🏋️‍♂️ AI Workout Trainer")
194
-
195
  st.markdown("""
196
- <div class='workout-container'>
197
- Welcome to your AI Workout Trainer! This app will help you perfect your form
198
- and track your exercises in real-time. Choose a workout and follow the feedback
199
- to improve your technique.
 
 
 
200
  </div>
201
  """, unsafe_allow_html=True)
202
 
203
- # Workout selection
204
- workout_options = {
205
- "Bicep Curl": "bicep_curl",
206
- "Lateral Raise": "lateral_raise",
207
- "Shoulder Press": "shoulder_press"
208
- }
209
-
210
- selected_workout = st.selectbox(
211
- "Choose your workout:",
212
- list(workout_options.keys())
213
  )
214
 
215
- # Reset state when workout changes
216
- if 'last_workout' not in st.session_state or st.session_state.last_workout != selected_workout:
217
- with lock:
218
- state.counter = 0
219
- state.stage = None
220
- state.feedback = ""
221
- st.session_state.last_workout = selected_workout
222
-
223
- # Exercise descriptions
224
- descriptions = {
225
- "Bicep Curl": "Focus on keeping your upper arm still and curl the weight up smoothly.",
226
- "Lateral Raise": "Raise your arms to shoulder height, keeping them slightly bent.",
227
- "Shoulder Press": "Press the weight overhead, fully extending your arms."
228
- }
229
-
230
- st.markdown(f"""
231
- <div class='workout-container'>
232
- <h3>{selected_workout}</h3>
233
- <p>{descriptions[selected_workout]}</p>
234
- </div>
235
- """, unsafe_allow_html=True)
236
-
237
- # Initialize WebRTC streamer
238
- webrtc_ctx = webrtc_streamer(
239
- key="workout",
240
- video_transformer_factory=VideoTransformer,
241
- rtc_configuration={"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]}
242
  )
243
 
244
- if webrtc_ctx.video_transformer:
245
- webrtc_ctx.video_transformer.workout_type = workout_options[selected_workout]
 
246
 
247
- # Display feedback
248
- feedback_placeholder = st.empty()
249
- if webrtc_ctx.state.playing:
250
- feedback_placeholder.markdown(f"""
251
- <div class='feedback-text'>
252
- <h4>Current Exercise: {selected_workout}</h4>
253
- <p>Reps Completed: {state.counter}</p>
254
- <p>Feedback: {state.feedback}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  </div>
256
  """, unsafe_allow_html=True)
257
 
258
  if __name__ == "__main__":
259
- main()
 
 
 
 
 
 
 
 
260
 
 
1
  import streamlit as st
2
+ from streamlit_webrtc import webrtc_streamer, WebRtcMode, RTCConfiguration
 
 
 
3
  import av
4
+ import numpy as np
5
+ import mediapipe as mp
6
+ from typing import List, Tuple, Dict
7
+ import logging
8
+ from PIL import Image
9
+ import queue
10
  import threading
11
+ import logging
12
+ import sys
13
 
14
+ try:
15
+ from typing import Literal
16
+ except ImportError:
17
+ from typing_extensions import Literal
18
+
19
+ # Configure logging
20
+ logging.basicConfig(level=logging.DEBUG)
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Initialize MediaPipe Pose
24
  mp_pose = mp.solutions.pose
25
+ mp_drawing = mp.solutions.drawing_utils
26
 
27
+ # Page config
28
+ st.set_page_config(page_title="AI Workout Trainer", page_icon="💪")
29
+
30
+ # Constants
31
+ RTC_CONFIGURATION = RTCConfiguration(
32
+ {"iceServers": [{"urls": ["stun:stun.l.google.com:19302"]}]}
33
+ )
34
+
35
+ # CSS
36
  st.markdown("""
37
  <style>
38
  .main {
39
+ background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
40
  }
41
  .stButton > button {
42
+ width: 100%;
43
+ background-color: #2a5298;
44
  color: white;
 
 
45
  border-radius: 5px;
46
+ padding: 10px;
47
+ margin: 5px 0;
 
 
 
 
 
 
48
  }
49
  .workout-container {
50
+ background-color: rgba(255, 255, 255, 0.1);
51
+ padding: 20px;
52
  border-radius: 10px;
53
+ margin: 10px 0;
54
  }
55
+ h1, h2, h3 {
56
+ color: white;
 
 
 
57
  }
58
  </style>
59
  """, unsafe_allow_html=True)
60
 
61
+ class PoseTracker:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  def __init__(self):
63
+ self.pose = mp_pose.Pose(
64
+ min_detection_confidence=0.5,
65
+ min_tracking_confidence=0.5
66
+ )
67
+ self.counter = 0
68
+ self.stage = None
69
+ self.feedback = ""
70
+
71
+ def calculate_angle(self, a: np.ndarray, b: np.ndarray, c: np.ndarray) -> float:
72
+ a = np.array(a)
73
+ b = np.array(b)
74
+ c = np.array(c)
75
+
76
+ radians = np.arctan2(c[1]-b[1], c[0]-b[0]) - \
77
+ np.arctan2(a[1]-b[1], a[0]-b[0])
78
+ angle = np.abs(np.degrees(radians))
79
 
80
+ if angle > 180.0:
81
+ angle = 360-angle
82
+
83
+ return angle
84
+
85
+ def process_frame(self, frame: np.ndarray, exercise_type: str) -> Tuple[np.ndarray, str]:
86
+ try:
87
+ # Convert BGR to RGB
88
+ image = frame.copy()
89
+ image.flags.writeable = False
90
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
91
+ results = self.pose.process(image)
92
+
93
+ # Draw landmarks
94
+ image.flags.writeable = True
95
+ image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
96
+
97
+ if results.pose_landmarks:
98
+ mp_drawing.draw_landmarks(
99
+ image,
100
+ results.pose_landmarks,
101
+ mp_pose.POSE_CONNECTIONS
102
+ )
103
+
104
+ # Extract landmarks
105
+ try:
106
+ landmarks = results.pose_landmarks.landmark
107
+
108
+ # Process based on exercise type
109
+ if exercise_type == "bicep_curl":
110
+ feedback = self._process_bicep_curl(landmarks, image)
111
+ elif exercise_type == "shoulder_press":
112
+ feedback = self._process_shoulder_press(landmarks, image)
113
+ elif exercise_type == "lateral_raise":
114
+ feedback = self._process_lateral_raise(landmarks, image)
115
+ else:
116
+ feedback = "Unknown exercise type"
117
+
118
+ return image, feedback
119
+
120
+ except Exception as e:
121
+ logger.error(f"Error processing landmarks: {str(e)}")
122
+ return image, "Error processing pose"
123
+
124
+ return image, "No pose detected"
125
+
126
+ except Exception as e:
127
+ logger.error(f"Error in process_frame: {str(e)}")
128
+ return frame, "Error processing frame"
129
+
130
+ def _process_bicep_curl(self, landmarks, image) -> str:
131
+ # Get coordinates
132
  shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,
133
  landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y]
134
  elbow = [landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x,
 
136
  wrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,
137
  landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y]
138
 
139
+ # Calculate angle
140
+ angle = self.calculate_angle(shoulder, elbow, wrist)
 
 
 
 
 
 
 
 
141
 
142
+ # Counter logic
143
+ if angle > 160:
144
+ self.stage = "down"
145
+ elif angle < 30 and self.stage == "down":
146
+ self.stage = "up"
147
+ self.counter += 1
148
+
149
+ # Add text to image
150
+ cv2.putText(image, f'Angle: {angle:.2f}', (10,30),
151
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
152
+ cv2.putText(image, f'Count: {self.counter}', (10,60),
153
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
154
+
155
+ if angle < 160 and self.stage == "down":
156
+ return "Curl up"
157
+ elif angle > 30 and self.stage == "up":
158
+ return "Lower the weight"
159
+ else:
160
+ return f"Count: {self.counter}"
161
 
162
+ def _process_shoulder_press(self, landmarks, image) -> str:
163
+ # Similar structure to bicep curl but with shoulder press specific angles
164
  shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,
165
  landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y]
166
+ elbow = [landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x,
167
+ landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].y]
168
  wrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,
169
  landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y]
170
 
171
+ angle = self.calculate_angle(shoulder, elbow, wrist)
172
 
173
+ if angle < 90:
174
+ self.stage = "down"
175
+ elif angle > 160 and self.stage == "down":
176
+ self.stage = "up"
177
+ self.counter += 1
178
+
179
+ cv2.putText(image, f'Angle: {angle:.2f}', (10,30),
180
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
181
+ cv2.putText(image, f'Count: {self.counter}', (10,60),
182
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
183
 
184
+ if angle > 90 and self.stage == "down":
185
+ return "Press up fully"
186
+ elif angle < 160 and self.stage == "up":
187
+ return "Lower the weight"
188
+ else:
189
+ return f"Count: {self.counter}"
190
 
191
+ def _process_lateral_raise(self, landmarks, image) -> str:
 
192
  shoulder = [landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].x,
193
  landmarks[mp_pose.PoseLandmark.LEFT_SHOULDER.value].y]
194
  elbow = [landmarks[mp_pose.PoseLandmark.LEFT_ELBOW.value].x,
 
196
  wrist = [landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].x,
197
  landmarks[mp_pose.PoseLandmark.LEFT_WRIST.value].y]
198
 
199
+ angle = self.calculate_angle(shoulder, elbow, wrist)
200
 
201
+ if angle < 20:
202
+ self.stage = "down"
203
+ elif angle > 80 and self.stage == "down":
204
+ self.stage = "up"
205
+ self.counter += 1
206
+
207
+ cv2.putText(image, f'Angle: {angle:.2f}', (10,30),
208
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
209
+ cv2.putText(image, f'Count: {self.counter}', (10,60),
210
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
211
 
212
+ if angle < 80 and self.stage == "down":
213
+ return "Raise arms higher"
214
+ elif angle > 20 and self.stage == "up":
215
+ return "Lower arms"
216
+ else:
217
+ return f"Count: {self.counter}"
218
 
219
+ class VideoProcessor:
220
+ def __init__(self) -> None:
221
+ self.pose_tracker = PoseTracker()
222
+ self._exercise_type = "bicep_curl"
223
+
224
+ @property
225
+ def exercise_type(self) -> str:
226
+ return self._exercise_type
227
 
228
+ @exercise_type.setter
229
+ def exercise_type(self, value: str) -> None:
230
+ self._exercise_type = value
231
 
232
+ def recv(self, frame: av.VideoFrame) -> av.VideoFrame:
233
+ try:
234
+ img = frame.to_ndarray(format="bgr24")
 
 
 
 
235
 
236
+ # Process the frame
237
+ processed_frame, feedback = self.pose_tracker.process_frame(img, self.exercise_type)
 
 
 
 
 
238
 
239
+ # Add feedback to frame
240
+ cv2.putText(processed_frame, feedback, (10,90),
241
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255), 2)
242
+
243
+ return av.VideoFrame.from_ndarray(processed_frame, format="bgr24")
244
+ except Exception as e:
245
+ logger.error(f"Error in recv: {str(e)}")
246
+ return frame
 
247
 
248
  def main():
249
+ # Title and description
250
  st.title("🏋️‍♂️ AI Workout Trainer")
 
251
  st.markdown("""
252
+ <div class="workout-container">
253
+ <p>Welcome to your personal AI workout trainer! This app will help you:
254
+ <ul>
255
+ <li>Track your exercise form in real-time</li>
256
+ <li>Count your reps automatically</li>
257
+ <li>Provide instant feedback on your technique</li>
258
+ </ul></p>
259
  </div>
260
  """, unsafe_allow_html=True)
261
 
262
+ # Exercise selection
263
+ exercise_type = st.selectbox(
264
+ "Choose your exercise:",
265
+ ["bicep_curl", "shoulder_press", "lateral_raise"],
266
+ format_func=lambda x: x.replace('_', ' ').title()
 
 
 
 
 
267
  )
268
 
269
+ # Create VideoProcessor instance
270
+ ctx = webrtc_streamer(
271
+ key="workout-tracker",
272
+ mode=WebRtcMode.SENDRECV,
273
+ rtc_configuration=RTC_CONFIGURATION,
274
+ video_processor_factory=VideoProcessor,
275
+ media_stream_constraints={"video": True, "audio": False},
276
+ async_processing=True,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  )
278
 
279
+ # Update exercise type when changed
280
+ if ctx.video_processor:
281
+ ctx.video_processor.exercise_type = exercise_type
282
 
283
+ # Instructions based on exercise
284
+ if exercise_type == "bicep_curl":
285
+ st.markdown("""
286
+ <div class="workout-container">
287
+ <h3>Bicep Curl Instructions:</h3>
288
+ <ul>
289
+ <li>Stand straight with weights at your sides</li>
290
+ <li>Keep your upper arms still</li>
291
+ <li>Curl the weights up towards your shoulders</li>
292
+ <li>Lower the weights back down controlled</li>
293
+ </ul>
294
+ </div>
295
+ """, unsafe_allow_html=True)
296
+ elif exercise_type == "shoulder_press":
297
+ st.markdown("""
298
+ <div class="workout-container">
299
+ <h3>Shoulder Press Instructions:</h3>
300
+ <ul>
301
+ <li>Start with weights at shoulder height</li>
302
+ <li>Press weights straight up overhead</li>
303
+ <li>Keep your core tight</li>
304
+ <li>Lower weights back to shoulders controlled</li>
305
+ </ul>
306
+ </div>
307
+ """, unsafe_allow_html=True)
308
+ else: # lateral_raise
309
+ st.markdown("""
310
+ <div class="workout-container">
311
+ <h3>Lateral Raise Instructions:</h3>
312
+ <ul>
313
+ <li>Stand straight with weights at your sides</li>
314
+ <li>Raise arms out to sides up to shoulder height</li>
315
+ <li>Keep a slight bend in your elbows</li>
316
+ <li>Lower weights back down controlled</li>
317
+ </ul>
318
  </div>
319
  """, unsafe_allow_html=True)
320
 
321
  if __name__ == "__main__":
322
+ try:
323
+ # Import OpenCV
324
+ import cv2
325
+ main()
326
+ except Exception as e:
327
+ st.error(f"An error occurred: {str(e)}")
328
+ if "cv2" in str(e):
329
+ st.warning("OpenCV import failed. Please check your installation.")
330
+ logger.error(f"Application error: {str(e)}")
331