Sean Carnahan commited on
Commit
f72c2f8
·
1 Parent(s): f20fe1f

Fix video processing and client-side issues

Browse files
Files changed (2) hide show
  1. HFup/app.py +334 -339
  2. HFup/templates/index.html +179 -0
HFup/app.py CHANGED
@@ -1,3 +1,8 @@
 
 
 
 
 
1
  from flask import Flask, render_template, request, jsonify, send_from_directory, url_for
2
  from flask_cors import CORS
3
  import cv2
@@ -7,100 +12,156 @@ import os
7
  from werkzeug.utils import secure_filename
8
  import sys
9
  import traceback
 
10
  from tensorflow.keras.models import load_model
11
  from tensorflow.keras.preprocessing import image
12
  import time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  # Add bodybuilding_pose_analyzer to path
15
  sys.path.append('.') # Assuming app.py is at the root of cv.github.io
16
  from bodybuilding_pose_analyzer.src.movenet_analyzer import MoveNetAnalyzer
17
  from bodybuilding_pose_analyzer.src.pose_analyzer import PoseAnalyzer
18
 
19
- # Add YOLOv7 to path
20
- sys.path.append('yolov7')
21
-
22
- from yolov7.models.experimental import attempt_load
23
- from yolov7.utils.general import check_img_size, non_max_suppression_kpt, scale_coords
24
- from yolov7.utils.torch_utils import select_device
25
- from yolov7.utils.plots import plot_skeleton_kpts
26
-
27
- def wrap_text(text: str, font_face: int, font_scale: float, thickness: int, max_width: int) -> list[str]:
28
- """Wrap text to fit within max_width."""
29
- if not text:
30
- return []
31
-
32
- lines = []
33
- words = text.split(' ')
34
- current_line = ''
35
-
36
- for word in words:
37
- # Check width if current_line + word fits
38
- test_line = current_line + word + ' '
39
- (text_width, _), _ = cv2.getTextSize(test_line.strip(), font_face, font_scale, thickness)
40
 
41
- if text_width <= max_width:
42
- current_line = test_line
43
- else:
44
- # Word doesn't fit, so current_line (without the new word) is a complete line
45
- lines.append(current_line.strip())
46
- # Start new line with the current word
47
- current_line = word + ' '
48
- # If a single word is too long, it will still overflow. Handle by breaking word if necessary (future enhancement)
49
- (single_word_width, _), _ = cv2.getTextSize(word.strip(), font_face, font_scale, thickness)
50
- if single_word_width > max_width:
51
- # For now, just add the long word and let it overflow, or truncate it.
52
- # A more complex solution would break the word.
53
- lines.append(word.strip()) # Add the long word as its own line
54
- current_line = '' # Reset current_line as the long word is handled
55
-
56
- if current_line.strip(): # Add the last line
57
- lines.append(current_line.strip())
58
-
59
- return lines if lines else [text] # Ensure at least the original text is returned if no wrapping happens
60
 
61
- app = Flask(__name__, static_url_path='/static', static_folder='static')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  CORS(app, resources={r"/*": {"origins": "*"}})
63
 
64
- app.config['UPLOAD_FOLDER'] = 'static/uploads'
65
- app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
66
-
67
- # Ensure upload directory exists
68
- os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
69
-
70
- # Initialize YOLOv7 model
71
- device = select_device('')
72
- yolo_model = None # Initialize as None
73
- stride = None
74
- imgsz = None
75
 
 
76
  try:
77
- yolo_model = attempt_load('yolov7-w6-pose.pt', map_location=device)
78
- stride = int(yolo_model.stride.max())
79
- imgsz = check_img_size(640, s=stride)
80
- print("YOLOv7 Model loaded successfully")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  except Exception as e:
82
- print(f"Error loading YOLOv7 model: {e}")
83
- traceback.print_exc()
84
- # Not raising here to allow app to run if only MoveNet is used. Error will be caught if YOLOv7 is selected.
85
 
86
- # YOLOv7 pose model expects 17 keypoints
87
- kpt_shape = (17, 3)
 
 
 
 
 
 
 
 
 
 
88
 
89
- # Load CNN model for bodybuilding pose classification
90
- cnn_model_path = 'external/BodybuildingPoseClassifier/bodybuilding_pose_classifier.h5'
91
- cnn_model = load_model(cnn_model_path)
92
  cnn_class_labels = ['side_chest', 'front_double_biceps', 'back_double_biceps', 'front_lat_spread', 'back_lat_spread']
93
 
94
  def predict_pose_cnn(img_path):
95
- img = image.load_img(img_path, target_size=(150, 150))
96
- img_array = image.img_to_array(img)
97
- img_array = np.expand_dims(img_array, axis=0) / 255.0
98
- predictions = cnn_model.predict(img_array)
99
- predicted_class = np.argmax(predictions, axis=1)
100
- confidence = float(np.max(predictions))
101
- return cnn_class_labels[predicted_class[0]], confidence
102
-
103
- @app.route('/static/uploads/<path:filename>')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  def serve_video(filename):
105
  response = send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=False)
106
  # Ensure correct content type, especially for Safari/iOS if issues arise
@@ -115,256 +176,157 @@ def after_request(response):
115
  response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
116
  return response
117
 
118
- def process_video_yolov7(video_path): # Renamed from process_video
119
- global yolo_model, imgsz, stride # Ensure global model is used
120
- if yolo_model is None:
121
- raise RuntimeError("YOLOv7 model failed to load. Cannot process video.")
122
  try:
123
- if not os.path.exists(video_path):
124
- raise FileNotFoundError(f"Video file not found: {video_path}")
125
-
126
- cap = cv2.VideoCapture(video_path)
127
- if not cap.isOpened():
128
- raise ValueError(f"Failed to open video file: {video_path}")
129
-
130
- fps = int(cap.get(cv2.CAP_PROP_FPS))
131
- width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
132
- height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
133
-
134
- print(f"Processing video: {width}x{height} @ {fps}fps")
135
 
136
- # Create output video writer
137
- output_path = os.path.join(app.config['UPLOAD_FOLDER'], 'output.mp4')
138
- fourcc = cv2.VideoWriter_fourcc(*'avc1')
139
- out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
140
 
141
- frame_count = 0
142
- while cap.isOpened():
143
- ret, frame = cap.read()
144
- if not ret:
145
- break
146
-
147
- frame_count += 1
148
- print(f"Processing frame {frame_count}")
149
-
150
- # Prepare image
151
- img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
152
- img = cv2.resize(img, (imgsz, imgsz))
153
- img = img.transpose((2, 0, 1)) # HWC to CHW
154
- img = np.ascontiguousarray(img)
155
- img = torch.from_numpy(img).to(device)
156
- img = img.float() / 255.0
157
- if img.ndimension() == 3:
158
- img = img.unsqueeze(0)
159
-
160
- # Inference
161
- with torch.no_grad():
162
- pred = yolo_model(img)[0] # Use yolo_model
163
- pred = non_max_suppression_kpt(pred, conf_thres=0.25, iou_thres=0.45, nc=yolo_model.yaml['nc'], kpt_label=True)
164
-
165
- # Draw results
166
- output_frame = frame.copy()
167
- poses_detected = False
168
- for det in pred:
169
- if len(det):
170
- poses_detected = True
171
- det[:, :4] = scale_coords(img.shape[2:], det[:, :4], frame.shape).round()
172
- for row in det:
173
- xyxy = row[:4]
174
- conf = row[4]
175
- cls = row[5]
176
- kpts = row[6:]
177
- kpts = torch.tensor(kpts).view(kpt_shape)
178
- output_frame = plot_skeleton_kpts(output_frame, kpts, steps=3, orig_shape=output_frame.shape[:2])
179
-
180
- if not poses_detected:
181
- print(f"No poses detected in frame {frame_count}")
182
-
183
- out.write(output_frame)
184
-
185
- cap.release()
186
- out.release()
187
-
188
- if frame_count == 0:
189
- raise ValueError("No frames were processed from the video")
190
-
191
- print(f"Video processing completed. Processed {frame_count} frames")
192
- # Return URL for the client, using the 'serve_video' endpoint
193
- output_filename = 'output.mp4'
194
- return url_for('serve_video', filename=output_filename, _external=False)
195
- except Exception as e:
196
- print('Error in process_video:', e)
197
- traceback.print_exc()
198
- raise
199
-
200
- def process_video_movenet(video_path, model_variant='lightning', pose_type='front_double_biceps'):
201
- try:
202
- print(f"[PROCESS_VIDEO_MOVENET] Called with video_path: {video_path}, model_variant: {model_variant}, pose_type: {pose_type}")
203
- if not os.path.exists(video_path):
204
- raise FileNotFoundError(f"Video file not found: {video_path}")
205
-
206
- analyzer = MoveNetAnalyzer(model_name=model_variant)
207
  cap = cv2.VideoCapture(video_path)
208
  if not cap.isOpened():
209
- raise ValueError(f"Failed to open video file: {video_path}")
 
 
 
210
  fps = int(cap.get(cv2.CAP_PROP_FPS))
211
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
212
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
 
 
213
 
214
- # Add panel width to total width
215
- panel_width = 300
216
- total_width = width + panel_width
 
 
 
 
 
 
 
 
 
217
 
218
- print(f"Processing video with MoveNet ({model_variant}): {width}x{height} @ {fps}fps")
219
- print(f"Output dimensions will be: {total_width}x{height}")
220
- output_filename = f'output_movenet_{model_variant}.mp4'
221
  output_path = os.path.join(app.config['UPLOAD_FOLDER'], output_filename)
222
  print(f"Output path: {output_path}")
223
 
224
- fourcc = cv2.VideoWriter_fourcc(*'avc1')
225
- out = cv2.VideoWriter(output_path, fourcc, fps, (total_width, height))
226
- if not out.isOpened():
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  raise ValueError(f"Failed to create output video writer at {output_path}")
228
-
229
  frame_count = 0
230
- current_pose = pose_type
231
- segment_length = 4 * fps if fps > 0 else 120
232
- cnn_pose = None
233
- last_valid_landmarks = None
234
- landmarks_analysis = {'error': 'Processing not started'} # Initialize landmarks_analysis
235
 
236
  while cap.isOpened():
237
- ret, frame = cap.read()
238
- if not ret:
239
- break
240
- frame_count += 1
241
- if frame_count % 30 == 0:
242
- print(f"Processing frame {frame_count}")
243
-
244
- # Process frame
245
- processed_frame, current_landmarks_analysis, landmarks = analyzer.process_frame(frame, current_pose, last_valid_landmarks=last_valid_landmarks)
246
- landmarks_analysis = current_landmarks_analysis # Update with the latest analysis
247
- if frame_count % 30 == 0: # Log every 30 frames
248
- print(f"[MOVENET_DEBUG] Frame {frame_count} - landmarks_analysis: {landmarks_analysis}")
249
- if landmarks:
250
- last_valid_landmarks = landmarks
251
-
252
- # CNN prediction (every 4 seconds)
253
- if (frame_count - 1) % segment_length == 0:
254
- temp_img_path = f'temp_frame_for_cnn_{frame_count}.jpg' # Unique temp name
255
- cv2.imwrite(temp_img_path, frame)
 
 
 
 
256
  try:
257
- cnn_pose_pred, cnn_conf = predict_pose_cnn(temp_img_path)
258
- print(f"[CNN] Frame {frame_count}: Pose: {cnn_pose_pred}, Conf: {cnn_conf:.2f}")
259
- if cnn_conf >= 0.3:
260
- current_pose = cnn_pose_pred # Update current_pose for the analyzer
261
  except Exception as e:
262
- print(f"[CNN] Error predicting pose on frame {frame_count}: {e}")
263
- finally:
264
- if os.path.exists(temp_img_path):
265
- os.remove(temp_img_path)
266
-
267
- # Create side panel
268
- panel = np.zeros((height, panel_width, 3), dtype=np.uint8)
269
-
270
- # --- Dynamic Text Parameter Calculations ---
271
- current_font = cv2.FONT_HERSHEY_DUPLEX
272
-
273
- # Base font scale and reference video height for scaling
274
- # Adjust base_font_scale_at_ref_height if text is generally too large or too small
275
- base_font_scale_at_ref_height = 0.6
276
- reference_height_for_font_scale = 640.0 # e.g., a common video height like 480p, 720p
277
-
278
- # Calculate dynamic font_scale
279
- font_scale = (height / reference_height_for_font_scale) * base_font_scale_at_ref_height
280
- # Clamp font_scale to a min/max range to avoid extremes
281
- font_scale = max(0.4, min(font_scale, 1.2))
282
-
283
- # Calculate dynamic thickness
284
- thickness = 1 if font_scale < 0.7 else 2
285
-
286
- # Calculate dynamic line_height based on actual text height
287
- # Using a sample string like "Ag" which has ascenders and descenders
288
- (_, text_actual_height), _ = cv2.getTextSize("Ag", current_font, font_scale, thickness)
289
- line_spacing_factor = 1.8 # Adjust for more or less space between lines
290
- line_height = int(text_actual_height * line_spacing_factor)
291
- line_height = max(line_height, 15) # Ensure a minimum line height
292
-
293
- # Initial y_offset for the first line of text
294
- y_offset_panel = max(line_height, 20) # Start considering top margin and text height
295
- # --- End of Dynamic Text Parameter Calculations ---
296
-
297
- display_model_name = f"Gladiator {model_variant.capitalize()}"
298
- cv2.putText(panel, f"Model: {display_model_name}", (10, y_offset_panel), current_font, font_scale, (0, 255, 255), thickness, lineType=cv2.LINE_AA)
299
- y_offset_panel += line_height
300
-
301
- if 'error' not in landmarks_analysis:
302
- cv2.putText(panel, "Angles:", (10, y_offset_panel), current_font, font_scale, (255, 255, 255), thickness, lineType=cv2.LINE_AA)
303
- y_offset_panel += line_height
304
- for joint, angle in landmarks_analysis.get('angles', {}).items():
305
- text_to_display = f"{joint.capitalize()}: {angle:.1f} deg"
306
- cv2.putText(panel, text_to_display, (20, y_offset_panel), current_font, font_scale, (0, 255, 0), thickness, lineType=cv2.LINE_AA)
307
- y_offset_panel += line_height
308
 
309
- # Define available width for text within the panel, considering padding
310
- text_area_x_start = 20
311
- panel_padding = 10 # Padding from the right edge of the panel
312
- text_area_width = panel_width - text_area_x_start - panel_padding
313
-
314
- if landmarks_analysis.get('corrections'):
315
- y_offset_panel += int(line_height * 0.5) # Smaller gap before section title
316
- cv2.putText(panel, "Corrections:", (10, y_offset_panel), current_font, font_scale, (255, 255, 255), thickness, lineType=cv2.LINE_AA)
317
- y_offset_panel += line_height
318
- for correction_text in landmarks_analysis.get('corrections', []):
319
- wrapped_lines = wrap_text(correction_text, current_font, font_scale, thickness, text_area_width)
320
- for line in wrapped_lines:
321
- cv2.putText(panel, line, (text_area_x_start, y_offset_panel), current_font, font_scale, (0, 0, 255), thickness, lineType=cv2.LINE_AA)
322
- y_offset_panel += line_height
 
 
 
323
 
324
- # Display notes if any
325
- if landmarks_analysis.get('notes'):
326
- y_offset_panel += int(line_height * 0.5) # Smaller gap before section title
327
- cv2.putText(panel, "Notes:", (10, y_offset_panel), current_font, font_scale, (200, 200, 200), thickness, lineType=cv2.LINE_AA)
328
- y_offset_panel += line_height
329
- for note_text in landmarks_analysis.get('notes', []):
330
- wrapped_lines = wrap_text(note_text, current_font, font_scale, thickness, text_area_width)
331
- for line in wrapped_lines:
332
- cv2.putText(panel, line, (text_area_x_start, y_offset_panel), current_font, font_scale, (200, 200, 200), thickness, lineType=cv2.LINE_AA)
333
- y_offset_panel += line_height
334
- else:
335
- cv2.putText(panel, "Error:", (10, y_offset_panel), current_font, font_scale, (255, 255, 255), thickness, lineType=cv2.LINE_AA)
336
- y_offset_panel += line_height
337
- # Also wrap error message if it can be long
338
- error_text = landmarks_analysis.get('error', 'Unknown error')
339
- text_area_x_start = 20 # Assuming error message also starts at x=20
340
- panel_padding = 10
341
- text_area_width = panel_width - text_area_x_start - panel_padding
342
- wrapped_error_lines = wrap_text(error_text, current_font, font_scale, thickness, text_area_width)
343
- for line in wrapped_error_lines:
344
- cv2.putText(panel, line, (text_area_x_start, y_offset_panel), current_font, font_scale, (0, 0, 255), thickness, lineType=cv2.LINE_AA)
345
- y_offset_panel += line_height
346
-
347
- combined_frame = np.hstack((processed_frame, panel))
348
- out.write(combined_frame)
349
-
350
  cap.release()
351
  out.release()
352
 
353
- if frame_count == 0:
354
- raise ValueError("No frames were processed from the video by MoveNet")
 
 
 
 
355
 
356
- print(f"MoveNet video processing completed. Processed {frame_count} frames. Output: {output_path}")
357
- print(f"Output file size: {os.path.getsize(output_path)} bytes")
 
 
 
 
 
 
 
 
 
 
 
358
 
359
- return url_for('serve_video', filename=output_filename, _external=False)
360
  except Exception as e:
361
- print(f'Error in process_video_movenet: {e}')
362
- traceback.print_exc()
363
  raise
364
 
365
  def process_video_mediapipe(video_path):
366
  try:
367
- print(f"[PROCESS_VIDEO_MEDIAPIPE] Called with video_path: {video_path}")
 
368
  if not os.path.exists(video_path):
369
  raise FileNotFoundError(f"Video file not found: {video_path}")
370
 
@@ -383,7 +345,7 @@ def process_video_mediapipe(video_path):
383
  print(f"Processing video with MediaPipe: {width}x{height} @ {fps}fps")
384
  output_filename = f'output_mediapipe.mp4'
385
  output_path = os.path.join(app.config['UPLOAD_FOLDER'], output_filename)
386
- fourcc = cv2.VideoWriter_fourcc(*'avc1')
387
  out = cv2.VideoWriter(output_path, fourcc, fps, (total_width, height))
388
  if not out.isOpened():
389
  raise ValueError(f"Failed to create output video writer at {output_path}")
@@ -401,7 +363,8 @@ def process_video_mediapipe(video_path):
401
  break
402
  frame_count += 1
403
  if frame_count % 30 == 0:
404
- print(f"Processing frame {frame_count}")
 
405
 
406
  # Process frame with MediaPipe
407
  processed_frame, current_analysis_results, landmarks = analyzer.process_frame(frame, last_valid_landmarks=last_valid_landmarks)
@@ -415,11 +378,11 @@ def process_video_mediapipe(video_path):
415
  cv2.imwrite(temp_img_path, frame)
416
  try:
417
  cnn_pose_pred, cnn_conf = predict_pose_cnn(temp_img_path)
418
- print(f"[CNN] Frame {frame_count}: Pose: {cnn_pose_pred}, Conf: {cnn_conf:.2f}")
419
  if cnn_conf >= 0.3:
420
  current_pose = cnn_pose_pred # Update current_pose to be displayed
421
  except Exception as e:
422
- print(f"[CNN] Error predicting pose on frame {frame_count}: {e}")
423
  finally:
424
  if os.path.exists(temp_img_path):
425
  os.remove(temp_img_path)
@@ -431,33 +394,29 @@ def process_video_mediapipe(video_path):
431
  current_font = cv2.FONT_HERSHEY_DUPLEX
432
 
433
  # Base font scale and reference video height for scaling
434
- # Adjust base_font_scale_at_ref_height if text is generally too large or too small
435
  base_font_scale_at_ref_height = 0.6
436
- reference_height_for_font_scale = 640.0 # e.g., a common video height like 480p, 720p
437
 
438
  # Calculate dynamic font_scale
439
  font_scale = (height / reference_height_for_font_scale) * base_font_scale_at_ref_height
440
- # Clamp font_scale to a min/max range to avoid extremes
441
  font_scale = max(0.4, min(font_scale, 1.2))
442
 
443
  # Calculate dynamic thickness
444
  thickness = 1 if font_scale < 0.7 else 2
445
 
446
- # Calculate dynamic line_height based on actual text height
447
- # Using a sample string like "Ag" which has ascenders and descenders
448
  (_, text_actual_height), _ = cv2.getTextSize("Ag", current_font, font_scale, thickness)
449
- line_spacing_factor = 1.8 # Adjust for more or less space between lines
450
  line_height = int(text_actual_height * line_spacing_factor)
451
- line_height = max(line_height, 15) # Ensure a minimum line height
452
 
453
- # Initial y_offset for the first line of text
454
- y_offset_panel = max(line_height, 20) # Start considering top margin and text height
455
- # --- End of Dynamic Text Parameter Calculations ---
456
 
457
  cv2.putText(panel, "Model: Gladiator SupaDot", (10, y_offset_panel), current_font, font_scale, (0, 255, 255), thickness, lineType=cv2.LINE_AA)
458
  y_offset_panel += line_height
459
- if frame_count % 30 == 0: # Print every 30 frames to avoid flooding console
460
- print(f"[MEDIAPIPE_PANEL] Frame {frame_count} - Current Pose for Panel: {current_pose}")
461
  cv2.putText(panel, f"Pose: {current_pose}", (10, y_offset_panel), current_font, font_scale, (255, 0, 0), thickness, lineType=cv2.LINE_AA)
462
  y_offset_panel += int(line_height * 1.5)
463
 
@@ -477,10 +436,9 @@ def process_video_mediapipe(video_path):
477
  cv2.putText(panel, f"• {correction}", (20, y_offset_panel), current_font, font_scale, (0, 0, 255), thickness, lineType=cv2.LINE_AA)
478
  y_offset_panel += line_height
479
 
480
- # Display notes if any
481
  if analysis_results.get('notes'):
482
  y_offset_panel += line_height
483
- cv2.putText(panel, "Notes:", (10, y_offset_panel), current_font, font_scale, (200, 200, 200), thickness, lineType=cv2.LINE_AA) # Grey color for notes
484
  y_offset_panel += line_height
485
  for note in analysis_results.get('notes', []):
486
  cv2.putText(panel, f"• {note}", (20, y_offset_panel), current_font, font_scale, (200, 200, 200), thickness, lineType=cv2.LINE_AA)
@@ -490,76 +448,95 @@ def process_video_mediapipe(video_path):
490
  y_offset_panel += line_height
491
  cv2.putText(panel, analysis_results.get('error', 'Unknown error'), (20, y_offset_panel), current_font, font_scale, (0, 0, 255), thickness, lineType=cv2.LINE_AA)
492
 
493
- combined_frame = np.hstack((processed_frame, panel)) # Use processed_frame from analyzer
494
  out.write(combined_frame)
495
 
496
  cap.release()
497
  out.release()
 
498
  if frame_count == 0:
499
  raise ValueError("No frames were processed from the video by MediaPipe")
500
- print(f"MediaPipe video processing completed. Processed {frame_count} frames. Output: {output_path}")
501
- return url_for('serve_video', filename=output_filename, _external=False)
 
 
502
  except Exception as e:
503
- print(f'Error in process_video_mediapipe: {e}')
504
  traceback.print_exc()
505
  raise
 
 
506
 
507
  @app.route('/')
508
  def index():
509
  return render_template('index.html')
510
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  @app.route('/upload', methods=['POST'])
512
  def upload_file():
513
  try:
 
514
  if 'video' not in request.files:
515
- print("[UPLOAD] No video file in request")
516
  return jsonify({'error': 'No video file provided'}), 400
517
 
518
  file = request.files['video']
519
  if file.filename == '':
520
- print("[UPLOAD] Empty filename")
521
  return jsonify({'error': 'No selected file'}), 400
522
 
523
  if file:
524
  allowed_extensions = {'mp4', 'avi', 'mov', 'mkv'}
525
  if '.' not in file.filename or file.filename.rsplit('.', 1)[1].lower() not in allowed_extensions:
526
- print(f"[UPLOAD] Invalid file format: {file.filename}")
527
  return jsonify({'error': 'Invalid file format. Allowed formats: mp4, avi, mov, mkv'}), 400
528
 
529
  # Ensure the filename is properly sanitized
530
  filename = secure_filename(file.filename)
531
- print(f"[UPLOAD] Original filename: {file.filename}")
532
- print(f"[UPLOAD] Sanitized filename: {filename}")
533
 
534
  # Create a unique filename to prevent conflicts
535
  base, ext = os.path.splitext(filename)
536
  unique_filename = f"{base}_{int(time.time())}{ext}"
537
  filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
538
 
539
- print(f"[UPLOAD] Saving file to: {filepath}")
 
 
 
540
  file.save(filepath)
541
 
542
  if not os.path.exists(filepath):
543
- print(f"[UPLOAD] File not found after save: {filepath}")
544
  return jsonify({'error': 'Failed to save uploaded file'}), 500
545
 
546
- print(f"[UPLOAD] File saved successfully. Size: {os.path.getsize(filepath)} bytes")
547
 
548
  try:
549
  model_choice = request.form.get('model_choice', 'Gladiator SupaDot')
550
- print(f"[UPLOAD] Processing with model: {model_choice}")
551
 
552
- if model_choice == 'movenet':
553
- movenet_variant = request.form.get('movenet_variant', 'lightning')
554
- print(f"[UPLOAD] Using MoveNet variant: {movenet_variant}")
555
- output_path_url = process_video_movenet(filepath, model_variant=movenet_variant)
556
- else:
557
- output_path_url = process_video_mediapipe(filepath)
558
-
559
- print(f"[UPLOAD] Processing complete. Output URL: {output_path_url}")
560
 
561
- if not os.path.exists(os.path.join(app.config['UPLOAD_FOLDER'], os.path.basename(output_path_url))):
562
- print(f"[UPLOAD] Output file not found: {output_path_url}")
 
563
  return jsonify({'error': 'Output video file not found'}), 500
564
 
565
  return jsonify({
@@ -568,22 +545,40 @@ def upload_file():
568
  })
569
 
570
  except Exception as e:
571
- print(f"[UPLOAD] Error processing video: {str(e)}")
572
- traceback.print_exc()
573
  return jsonify({'error': f'Error processing video: {str(e)}'}), 500
574
 
575
  finally:
576
  try:
577
  if os.path.exists(filepath):
578
  os.remove(filepath)
579
- print(f"[UPLOAD] Cleaned up input file: {filepath}")
580
  except Exception as e:
581
- print(f"[UPLOAD] Error cleaning up file: {str(e)}")
582
 
583
  except Exception as e:
584
- print(f"[UPLOAD] Unexpected error: {str(e)}")
585
- traceback.print_exc()
586
  return jsonify({'error': 'Internal server error'}), 500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
587
 
588
  if __name__ == '__main__':
589
  # Ensure the port is 7860 and debug is False for HF Spaces deployment
 
1
+ # Patch for Hugging Face Spaces: set MPLCONFIGDIR to avoid permission errors with matplotlib
2
+ import os
3
+ os.environ["MPLCONFIGDIR"] = "/tmp/matplotlib"
4
+ os.makedirs("/tmp/matplotlib", exist_ok=True)
5
+
6
  from flask import Flask, render_template, request, jsonify, send_from_directory, url_for
7
  from flask_cors import CORS
8
  import cv2
 
12
  from werkzeug.utils import secure_filename
13
  import sys
14
  import traceback
15
+ import tensorflow as tf
16
  from tensorflow.keras.models import load_model
17
  from tensorflow.keras.preprocessing import image
18
  import time
19
+ import tensorflow_hub as hub
20
+ import gc
21
+ import psutil
22
+ import logging
23
+
24
+ # Check GPU availability
25
+ print("[GPU] Checking GPU availability...")
26
+ gpus = tf.config.list_physical_devices('GPU')
27
+ if gpus:
28
+ print(f"[GPU] Found {len(gpus)} GPU(s):")
29
+ for gpu in gpus:
30
+ print(f"[GPU] {gpu}")
31
+ # Enable memory growth to avoid allocating all GPU memory at once
32
+ for gpu in gpus:
33
+ tf.config.experimental.set_memory_growth(gpu, True)
34
+ print("[GPU] Memory growth enabled for all GPUs")
35
+ else:
36
+ print("[GPU] No GPU found, will use CPU")
37
 
38
  # Add bodybuilding_pose_analyzer to path
39
  sys.path.append('.') # Assuming app.py is at the root of cv.github.io
40
  from bodybuilding_pose_analyzer.src.movenet_analyzer import MoveNetAnalyzer
41
  from bodybuilding_pose_analyzer.src.pose_analyzer import PoseAnalyzer
42
 
43
+ # Configure logging
44
+ logging.basicConfig(
45
+ level=logging.INFO,
46
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
47
+ )
48
+ logger = logging.getLogger(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
+ def log_memory_usage():
51
+ """Log current memory usage."""
52
+ try:
53
+ process = psutil.Process()
54
+ memory_info = process.memory_info()
55
+ logger.info(f"Memory usage: {memory_info.rss / 1024 / 1024:.2f} MB")
56
+ except Exception as e:
57
+ logger.error(f"Error logging memory usage: {e}")
 
 
 
 
 
 
 
 
 
 
 
58
 
59
+ def cleanup_memory():
60
+ """Force garbage collection and log memory usage."""
61
+ try:
62
+ gc.collect()
63
+ log_memory_usage()
64
+ except Exception as e:
65
+ logger.error(f"Error in cleanup_memory: {e}")
66
+
67
+ # Add file handler for persistent logging
68
+ log_dir = 'logs'
69
+ os.makedirs(log_dir, exist_ok=True)
70
+ file_handler = logging.FileHandler(os.path.join(log_dir, 'app.log'))
71
+ file_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
72
+ logger.addHandler(file_handler)
73
+
74
+ # Define base paths
75
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
76
+ STATIC_DIR = os.path.join(BASE_DIR, 'static')
77
+ UPLOAD_DIR = os.path.join(STATIC_DIR, 'uploads')
78
+ MODEL_DIR = os.path.join(BASE_DIR, 'external', 'BodybuildingPoseClassifier')
79
+
80
+ # Ensure all required directories exist
81
+ for directory in [STATIC_DIR, UPLOAD_DIR, MODEL_DIR, log_dir]:
82
+ os.makedirs(directory, exist_ok=True)
83
+ logger.info(f"Ensured directory exists: {directory}")
84
+
85
+ app = Flask(__name__, static_url_path='/static', static_folder=STATIC_DIR)
86
  CORS(app, resources={r"/*": {"origins": "*"}})
87
 
88
+ app.config['UPLOAD_FOLDER'] = UPLOAD_DIR
89
+ app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 100MB max file size
 
 
 
 
 
 
 
 
 
90
 
91
+ # Load CNN model for bodybuilding pose classification
92
  try:
93
+ logger.info("Loading CNN model...")
94
+ cnn_model_path = os.path.join(MODEL_DIR, 'bodybuilding_pose_classifier.h5')
95
+ logger.info(f"Looking for model at: {cnn_model_path}")
96
+
97
+ # List directory contents to debug
98
+ logger.info(f"Contents of MODEL_DIR: {os.listdir(MODEL_DIR)}")
99
+
100
+ if not os.path.exists(cnn_model_path):
101
+ logger.error(f"Model file not found at {cnn_model_path}")
102
+ logger.error(f"Current working directory: {os.getcwd()}")
103
+ logger.error(f"Directory contents: {os.listdir('.')}")
104
+ raise FileNotFoundError(f"CNN model not found at {cnn_model_path}")
105
+
106
+ # Check file permissions
107
+ logger.info(f"Model file permissions: {oct(os.stat(cnn_model_path).st_mode)[-3:]}")
108
+
109
+ # Load model with custom_objects to handle any custom layers
110
+ logger.info("Attempting to load model...")
111
+ cnn_model = load_model(cnn_model_path, compile=False)
112
+ logger.info("CNN model loaded successfully")
113
  except Exception as e:
114
+ logger.error(f"Error loading CNN model: {e}")
115
+ logger.error(traceback.format_exc())
116
+ raise
117
 
118
+ # Initialize TensorFlow session with memory growth
119
+ try:
120
+ gpus = tf.config.list_physical_devices('GPU')
121
+ if gpus:
122
+ for gpu in gpus:
123
+ tf.config.experimental.set_memory_growth(gpu, True)
124
+ logger.info("GPU memory growth enabled")
125
+ else:
126
+ logger.info("No GPU found, using CPU")
127
+ except Exception as e:
128
+ logger.error(f"Error configuring GPU: {e}")
129
+ logger.error(traceback.format_exc())
130
 
 
 
 
131
  cnn_class_labels = ['side_chest', 'front_double_biceps', 'back_double_biceps', 'front_lat_spread', 'back_lat_spread']
132
 
133
  def predict_pose_cnn(img_path):
134
+ try:
135
+ cleanup_memory()
136
+ if gpus:
137
+ logger.info("[CNN_DEBUG] Using GPU for CNN prediction")
138
+ with tf.device('/GPU:0'):
139
+ img = image.load_img(img_path, target_size=(150, 150))
140
+ img_array = image.img_to_array(img)
141
+ img_array = np.expand_dims(img_array, axis=0) / 255.0
142
+ predictions = cnn_model.predict(img_array, verbose=0)
143
+ predicted_class = np.argmax(predictions, axis=1)
144
+ confidence = float(np.max(predictions))
145
+ else:
146
+ logger.info("[CNN_DEBUG] No GPU found, using CPU for CNN prediction")
147
+ with tf.device('/CPU:0'):
148
+ img = image.load_img(img_path, target_size=(150, 150))
149
+ img_array = image.img_to_array(img)
150
+ img_array = np.expand_dims(img_array, axis=0) / 255.0
151
+ predictions = cnn_model.predict(img_array, verbose=0)
152
+ predicted_class = np.argmax(predictions, axis=1)
153
+ confidence = float(np.max(predictions))
154
+
155
+ logger.info(f"[CNN_DEBUG] Prediction successful: {cnn_class_labels[predicted_class[0]]}")
156
+ return cnn_class_labels[predicted_class[0]], confidence
157
+ except Exception as e:
158
+ logger.error(f"[CNN_ERROR] Exception during CNN prediction: {e}")
159
+ logger.error(traceback.format_exc())
160
+ raise
161
+ finally:
162
+ cleanup_memory()
163
+
164
+ @app.route('/static/uploads/<path:filename>', endpoint='serve_video')
165
  def serve_video(filename):
166
  response = send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=False)
167
  # Ensure correct content type, especially for Safari/iOS if issues arise
 
176
  response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
177
  return response
178
 
179
+ def process_video_movenet(video_path):
 
 
 
180
  try:
181
+ print("[DEBUG] Starting MoveNet video processing")
182
+ print(f"[DEBUG] Python version: {sys.version}")
183
+ print(f"[DEBUG] OpenCV version: {cv2.__version__}")
184
+ print(f"[DEBUG] TensorFlow version: {tf.__version__}")
185
+ print(f"[DEBUG] Upload dir contents: {os.listdir(os.path.dirname(video_path))}")
186
+ print(f"[DEBUG] Current working dir: {os.getcwd()}")
 
 
 
 
 
 
187
 
188
+ # Ensure upload directory exists and has proper permissions
189
+ upload_dir = os.path.dirname(video_path)
190
+ os.makedirs(upload_dir, exist_ok=True)
191
+ os.chmod(upload_dir, 0o777)
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  cap = cv2.VideoCapture(video_path)
194
  if not cap.isOpened():
195
+ print(f"[ERROR] Could not open video file: {video_path}")
196
+ raise ValueError("Could not open video file")
197
+
198
+ # Get video properties
199
  fps = int(cap.get(cv2.CAP_PROP_FPS))
200
  width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
201
  height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
202
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
203
+ print(f"[DEBUG] Video properties - FPS: {fps}, Width: {width}, Height: {height}, Total Frames: {total_frames}")
204
 
205
+ # Force MoveNet to CPU to avoid GPU JIT error
206
+ print("[DEBUG] Forcing CPU for MoveNet (due to GPU JIT error)")
207
+ try:
208
+ with tf.device('/CPU:0'):
209
+ print("[DEBUG] Loading MoveNet model...")
210
+ movenet_model = hub.load("https://tfhub.dev/google/movenet/singlepose/lightning/4")
211
+ movenet = movenet_model.signatures['serving_default']
212
+ print("[DEBUG] MoveNet model loaded.")
213
+ except Exception as e:
214
+ print(f"[ERROR] Exception during MoveNet model load: {e}")
215
+ import traceback; traceback.print_exc()
216
+ raise
217
 
218
+ # Create output video writer with H.264 codec
219
+ output_filename = f'output_movenet_lightning.mp4'
 
220
  output_path = os.path.join(app.config['UPLOAD_FOLDER'], output_filename)
221
  print(f"Output path: {output_path}")
222
 
223
+ # Try different codecs in order of preference
224
+ codecs = ['mp4v', 'avc1', 'XVID']
225
+ out = None
226
+ for codec in codecs:
227
+ try:
228
+ fourcc = cv2.VideoWriter_fourcc(*codec)
229
+ out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
230
+ if out.isOpened():
231
+ print(f"[DEBUG] Successfully created video writer with codec: {codec}")
232
+ break
233
+ except Exception as e:
234
+ print(f"[DEBUG] Failed to create video writer with codec {codec}: {e}")
235
+ continue
236
+
237
+ if not out or not out.isOpened():
238
+ print(f"[ERROR] Failed to create output video writer at {output_path}")
239
  raise ValueError(f"Failed to create output video writer at {output_path}")
240
+
241
  frame_count = 0
242
+ processed_frames = 0
243
+ first_frame_shape = None
244
+ print("[DEBUG] Entering frame loop...")
 
 
245
 
246
  while cap.isOpened():
247
+ try:
248
+ ret, frame = cap.read()
249
+ if not ret or frame is None:
250
+ print(f"[DEBUG] Stopping at frame {frame_count+1}: ret={ret}, frame is None: {frame is None}")
251
+ break
252
+
253
+ if first_frame_shape is None:
254
+ first_frame_shape = frame.shape
255
+ print(f"[DEBUG] First frame shape: {first_frame_shape}")
256
+
257
+ frame_count += 1
258
+
259
+ # Ensure frame size matches VideoWriter
260
+ if frame.shape[1] != width or frame.shape[0] != height:
261
+ print(f"[WARNING] Frame size {frame.shape[1]}x{frame.shape[0]} does not match VideoWriter size {width}x{height}. Resizing.")
262
+ frame = cv2.resize(frame, (width, height))
263
+
264
+ # Resize and pad the image to keep aspect ratio
265
+ img = frame.copy()
266
+ img = tf.image.resize_with_pad(tf.expand_dims(img, axis=0), 192, 192)
267
+ img = tf.cast(img, dtype=tf.int32)
268
+
269
+ # Always run inference on CPU
270
  try:
271
+ with tf.device('/CPU:0'):
272
+ results = movenet(img)
273
+ keypoints = results['output_0'].numpy()
 
274
  except Exception as e:
275
+ print(f"[ERROR] Exception during MoveNet inference on frame {frame_count}: {e}")
276
+ import traceback; traceback.print_exc()
277
+ continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
 
279
+ # Process keypoints and draw on frame
280
+ y, x, c = frame.shape
281
+ shaped = np.squeeze(keypoints)
282
+ for kp in range(17):
283
+ ky, kx, kp_conf = shaped[kp]
284
+ if kp_conf > 0.3:
285
+ cx, cy = int(kx * x), int(ky * y)
286
+ cv2.circle(frame, (cx, cy), 6, (0, 255, 0), -1)
287
+
288
+ out.write(frame)
289
+ processed_frames += 1
290
+ print(f"[DEBUG] Wrote frame {frame_count} to output video.")
291
+
292
+ except Exception as e:
293
+ print(f"[ERROR] Exception in frame loop at frame {frame_count+1}: {e}")
294
+ import traceback; traceback.print_exc()
295
+ continue
296
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  cap.release()
298
  out.release()
299
 
300
+ print(f"[DEBUG] Processed {processed_frames} frames out of {total_frames} total frames")
301
+
302
+ # Check output file size
303
+ if not os.path.exists(output_path):
304
+ print(f"[ERROR] Output video file was not created: {output_path}")
305
+ raise ValueError(f"Output video file was not created: {output_path}")
306
 
307
+ file_size = os.path.getsize(output_path)
308
+ print(f"[DEBUG] Output video file size: {file_size} bytes")
309
+
310
+ if processed_frames == 0 or file_size < 1000:
311
+ print(f"[ERROR] Output video file is empty or too small: {output_path}")
312
+ raise ValueError(f"Output video file is empty or too small: {output_path}")
313
+
314
+ # Ensure output file has proper permissions
315
+ os.chmod(output_path, 0o666)
316
+
317
+ video_url = url_for('serve_video', filename=output_filename, _external=False)
318
+ print(f"[DEBUG] Returning video URL: {video_url}")
319
+ return video_url
320
 
 
321
  except Exception as e:
322
+ print(f"[FATAL ERROR] Uncaught exception in process_video_movenet: {e}")
323
+ import traceback; traceback.print_exc()
324
  raise
325
 
326
  def process_video_mediapipe(video_path):
327
  try:
328
+ cleanup_memory() # Clean up before processing
329
+ logger.info(f"[PROCESS_VIDEO_MEDIAPIPE] Called with video_path: {video_path}")
330
  if not os.path.exists(video_path):
331
  raise FileNotFoundError(f"Video file not found: {video_path}")
332
 
 
345
  print(f"Processing video with MediaPipe: {width}x{height} @ {fps}fps")
346
  output_filename = f'output_mediapipe.mp4'
347
  output_path = os.path.join(app.config['UPLOAD_FOLDER'], output_filename)
348
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
349
  out = cv2.VideoWriter(output_path, fourcc, fps, (total_width, height))
350
  if not out.isOpened():
351
  raise ValueError(f"Failed to create output video writer at {output_path}")
 
363
  break
364
  frame_count += 1
365
  if frame_count % 30 == 0:
366
+ logger.info(f"Processing frame {frame_count}")
367
+ cleanup_memory() # Clean up periodically
368
 
369
  # Process frame with MediaPipe
370
  processed_frame, current_analysis_results, landmarks = analyzer.process_frame(frame, last_valid_landmarks=last_valid_landmarks)
 
378
  cv2.imwrite(temp_img_path, frame)
379
  try:
380
  cnn_pose_pred, cnn_conf = predict_pose_cnn(temp_img_path)
381
+ logger.info(f"[CNN] Frame {frame_count}: Pose: {cnn_pose_pred}, Conf: {cnn_conf:.2f}")
382
  if cnn_conf >= 0.3:
383
  current_pose = cnn_pose_pred # Update current_pose to be displayed
384
  except Exception as e:
385
+ logger.error(f"[CNN] Error predicting pose on frame {frame_count}: {e}")
386
  finally:
387
  if os.path.exists(temp_img_path):
388
  os.remove(temp_img_path)
 
394
  current_font = cv2.FONT_HERSHEY_DUPLEX
395
 
396
  # Base font scale and reference video height for scaling
 
397
  base_font_scale_at_ref_height = 0.6
398
+ reference_height_for_font_scale = 640.0
399
 
400
  # Calculate dynamic font_scale
401
  font_scale = (height / reference_height_for_font_scale) * base_font_scale_at_ref_height
 
402
  font_scale = max(0.4, min(font_scale, 1.2))
403
 
404
  # Calculate dynamic thickness
405
  thickness = 1 if font_scale < 0.7 else 2
406
 
407
+ # Calculate dynamic line_height
 
408
  (_, text_actual_height), _ = cv2.getTextSize("Ag", current_font, font_scale, thickness)
409
+ line_spacing_factor = 1.8
410
  line_height = int(text_actual_height * line_spacing_factor)
411
+ line_height = max(line_height, 15)
412
 
413
+ # Initial y_offset
414
+ y_offset_panel = max(line_height, 20)
 
415
 
416
  cv2.putText(panel, "Model: Gladiator SupaDot", (10, y_offset_panel), current_font, font_scale, (0, 255, 255), thickness, lineType=cv2.LINE_AA)
417
  y_offset_panel += line_height
418
+ if frame_count % 30 == 0:
419
+ logger.info(f"[MEDIAPIPE_PANEL] Frame {frame_count} - Current Pose for Panel: {current_pose}")
420
  cv2.putText(panel, f"Pose: {current_pose}", (10, y_offset_panel), current_font, font_scale, (255, 0, 0), thickness, lineType=cv2.LINE_AA)
421
  y_offset_panel += int(line_height * 1.5)
422
 
 
436
  cv2.putText(panel, f"• {correction}", (20, y_offset_panel), current_font, font_scale, (0, 0, 255), thickness, lineType=cv2.LINE_AA)
437
  y_offset_panel += line_height
438
 
 
439
  if analysis_results.get('notes'):
440
  y_offset_panel += line_height
441
+ cv2.putText(panel, "Notes:", (10, y_offset_panel), current_font, font_scale, (200, 200, 200), thickness, lineType=cv2.LINE_AA)
442
  y_offset_panel += line_height
443
  for note in analysis_results.get('notes', []):
444
  cv2.putText(panel, f"• {note}", (20, y_offset_panel), current_font, font_scale, (200, 200, 200), thickness, lineType=cv2.LINE_AA)
 
448
  y_offset_panel += line_height
449
  cv2.putText(panel, analysis_results.get('error', 'Unknown error'), (20, y_offset_panel), current_font, font_scale, (0, 0, 255), thickness, lineType=cv2.LINE_AA)
450
 
451
+ combined_frame = np.hstack((processed_frame, panel))
452
  out.write(combined_frame)
453
 
454
  cap.release()
455
  out.release()
456
+ cleanup_memory() # Clean up after processing
457
  if frame_count == 0:
458
  raise ValueError("No frames were processed from the video by MediaPipe")
459
+ logger.info(f"MediaPipe video processing completed. Processed {frame_count} frames. Output: {output_path}")
460
+ video_url = url_for('serve_video', filename=output_filename, _external=False)
461
+ print(f"[DEBUG] Returning video URL: {video_url}")
462
+ return video_url
463
  except Exception as e:
464
+ logger.error(f'Error in process_video_mediapipe: {e}')
465
  traceback.print_exc()
466
  raise
467
+ finally:
468
+ cleanup_memory() # Clean up in case of error
469
 
470
  @app.route('/')
471
  def index():
472
  return render_template('index.html')
473
 
474
+ # Add error handling for video processing
475
+ def safe_video_processing(video_path, model_choice):
476
+ """Wrapper function to handle video processing with proper cleanup."""
477
+ try:
478
+ if model_choice == 'movenet':
479
+ return process_video_movenet(video_path)
480
+ else:
481
+ return process_video_mediapipe(video_path)
482
+ except Exception as e:
483
+ logger.error(f"Error in video processing: {e}")
484
+ logger.error(traceback.format_exc())
485
+ raise
486
+ finally:
487
+ cleanup_memory()
488
+
489
  @app.route('/upload', methods=['POST'])
490
  def upload_file():
491
  try:
492
+ cleanup_memory()
493
  if 'video' not in request.files:
494
+ logger.error("[UPLOAD] No video file in request")
495
  return jsonify({'error': 'No video file provided'}), 400
496
 
497
  file = request.files['video']
498
  if file.filename == '':
499
+ logger.error("[UPLOAD] Empty filename")
500
  return jsonify({'error': 'No selected file'}), 400
501
 
502
  if file:
503
  allowed_extensions = {'mp4', 'avi', 'mov', 'mkv'}
504
  if '.' not in file.filename or file.filename.rsplit('.', 1)[1].lower() not in allowed_extensions:
505
+ logger.error(f"[UPLOAD] Invalid file format: {file.filename}")
506
  return jsonify({'error': 'Invalid file format. Allowed formats: mp4, avi, mov, mkv'}), 400
507
 
508
  # Ensure the filename is properly sanitized
509
  filename = secure_filename(file.filename)
510
+ logger.info(f"[UPLOAD] Original filename: {file.filename}")
511
+ logger.info(f"[UPLOAD] Sanitized filename: {filename}")
512
 
513
  # Create a unique filename to prevent conflicts
514
  base, ext = os.path.splitext(filename)
515
  unique_filename = f"{base}_{int(time.time())}{ext}"
516
  filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
517
 
518
+ # Ensure upload directory exists
519
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
520
+
521
+ logger.info(f"[UPLOAD] Saving file to: {filepath}")
522
  file.save(filepath)
523
 
524
  if not os.path.exists(filepath):
525
+ logger.error(f"[UPLOAD] File not found after save: {filepath}")
526
  return jsonify({'error': 'Failed to save uploaded file'}), 500
527
 
528
+ logger.info(f"[UPLOAD] File saved successfully. Size: {os.path.getsize(filepath)} bytes")
529
 
530
  try:
531
  model_choice = request.form.get('model_choice', 'Gladiator SupaDot')
532
+ logger.info(f"[UPLOAD] Processing with model: {model_choice}")
533
 
534
+ output_path_url = safe_video_processing(filepath, model_choice)
535
+ logger.info(f"[UPLOAD] Processing complete. Output URL: {output_path_url}")
 
 
 
 
 
 
536
 
537
+ output_path = os.path.join(app.config['UPLOAD_FOLDER'], os.path.basename(output_path_url))
538
+ if not os.path.exists(output_path):
539
+ logger.error(f"[UPLOAD] Output file not found: {output_path}")
540
  return jsonify({'error': 'Output video file not found'}), 500
541
 
542
  return jsonify({
 
545
  })
546
 
547
  except Exception as e:
548
+ logger.error(f"[UPLOAD] Error processing video: {str(e)}")
549
+ logger.error(traceback.format_exc())
550
  return jsonify({'error': f'Error processing video: {str(e)}'}), 500
551
 
552
  finally:
553
  try:
554
  if os.path.exists(filepath):
555
  os.remove(filepath)
556
+ logger.info(f"[UPLOAD] Cleaned up input file: {filepath}")
557
  except Exception as e:
558
+ logger.error(f"[UPLOAD] Error cleaning up file: {str(e)}")
559
 
560
  except Exception as e:
561
+ logger.error(f"[UPLOAD] Unexpected error: {str(e)}")
562
+ logger.error(traceback.format_exc())
563
  return jsonify({'error': 'Internal server error'}), 500
564
+ finally:
565
+ cleanup_memory()
566
+
567
+ # Add more specific error handlers
568
+ @app.errorhandler(413)
569
+ def request_entity_too_large(error):
570
+ logger.error(f"File too large: {error}")
571
+ return jsonify({'error': 'File too large. Maximum size is 100MB'}), 413
572
+
573
+ @app.errorhandler(500)
574
+ def internal_server_error(error):
575
+ logger.error(f"Internal server error: {error}")
576
+ return jsonify({'error': 'Internal server error. Please try again later.'}), 500
577
+
578
+ @app.errorhandler(404)
579
+ def not_found_error(error):
580
+ logger.error(f"Resource not found: {error}")
581
+ return jsonify({'error': 'Resource not found'}), 404
582
 
583
  if __name__ == '__main__':
584
  # Ensure the port is 7860 and debug is False for HF Spaces deployment
HFup/templates/index.html ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Bodybuilding Pose Analyzer</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
8
+ </head>
9
+ <body class="bg-gray-100 min-h-screen">
10
+ <div class="container mx-auto px-4 py-8">
11
+ <h1 class="text-4xl font-bold text-center mb-8">Bodybuilding Pose Analyzer</h1>
12
+
13
+ <div class="max-w-2xl mx-auto bg-white rounded-lg shadow-lg p-6">
14
+ <div class="mb-6">
15
+ <h2 class="text-2xl font-semibold mb-4">Upload Video</h2>
16
+ <form id="uploadForm" class="space-y-4">
17
+ <div class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center">
18
+ <input type="file" id="videoInput" accept="video/*" class="hidden">
19
+ <label for="videoInput" class="cursor-pointer">
20
+ <div class="text-gray-600">
21
+ <svg class="mx-auto h-12 w-12" stroke="currentColor" fill="none" viewBox="0 0 48 48">
22
+ <path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
23
+ </svg>
24
+ <p class="mt-1">Click to upload a video</p>
25
+ <p id="fileName" class="text-sm text-gray-500 mt-1"></p>
26
+ </div>
27
+ </label>
28
+ </div>
29
+
30
+ <div>
31
+ <label class="block text-sm font-medium text-gray-700">Choose Model:</label>
32
+ <div class="mt-1 flex rounded-md shadow-sm">
33
+ <div class="relative flex items-stretch flex-grow focus-within:z-10">
34
+ <label class="inline-flex items-center">
35
+ <input type="radio" class="form-radio" name="model_choice" value="movenet" checked>
36
+ <span class="ml-2">Gladiator BB</span>
37
+ </label>
38
+ <label class="inline-flex items-center ml-6">
39
+ <input type="radio" class="form-radio" name="model_choice" value="Gladiator SupaDot">
40
+ <span class="ml-2">Gladiator SupaDot</span>
41
+ </label>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ <div id="gladiatorBBOptions" class="space-y-4">
46
+ <div>
47
+ <label class="block text-sm font-medium text-gray-700">Gladiator BB Variant:</label>
48
+ <div class="mt-1 flex rounded-md shadow-sm">
49
+ <div class="relative flex items-stretch flex-grow focus-within:z-10">
50
+ <label class="inline-flex items-center">
51
+ <input type="radio" class="form-radio" name="movenet_variant" value="lightning" checked>
52
+ <span class="ml-2">Lightning (Faster, Less Accurate)</span>
53
+ </label>
54
+ <label class="inline-flex items-center ml-6">
55
+ <input type="radio" class="form-radio" name="movenet_variant" value="thunder">
56
+ <span class="ml-2">Thunder (Slower, More Accurate)</span>
57
+ </label>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ <button type="submit" class="w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition duration-200">
64
+ Process Video
65
+ </button>
66
+ </form>
67
+ </div>
68
+
69
+ <div id="result" class="hidden">
70
+ <h2 class="text-2xl font-semibold mb-4">Results</h2>
71
+ <div class="aspect-w-16 aspect-h-9">
72
+ <video id="outputVideo" controls class="w-full rounded-lg"></video>
73
+ </div>
74
+ </div>
75
+
76
+ <div id="loading" class="hidden">
77
+ <div class="flex items-center justify-center">
78
+ <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
79
+ </div>
80
+ <p class="text-center mt-4">Processing video...</p>
81
+ </div>
82
+ </div>
83
+ </div>
84
+
85
+ <script>
86
+ document.getElementById('videoInput').addEventListener('change', function() {
87
+ const fileName = this.files[0] ? this.files[0].name : 'No file selected';
88
+ document.getElementById('fileName').textContent = fileName;
89
+ });
90
+
91
+ document.querySelectorAll('input[name="model_choice"]').forEach(radio => {
92
+ radio.addEventListener('change', function() {
93
+ const gladiatorBBOptions = document.getElementById('gladiatorBBOptions');
94
+ if (this.value === 'movenet') {
95
+ gladiatorBBOptions.classList.remove('hidden');
96
+ } else {
97
+ gladiatorBBOptions.classList.add('hidden');
98
+ }
99
+ });
100
+ });
101
+
102
+ // Trigger change event on page load for the initially checked model_choice
103
+ document.querySelector('input[name="model_choice"]:checked').dispatchEvent(new Event('change'));
104
+
105
+ document.getElementById('uploadForm').addEventListener('submit', async (e) => {
106
+ e.preventDefault();
107
+
108
+ const fileInput = document.getElementById('videoInput');
109
+ const file = fileInput.files[0];
110
+
111
+ if (!file) {
112
+ alert('Please select a video file');
113
+ return;
114
+ }
115
+
116
+ const formData = new FormData();
117
+ formData.append('video', file);
118
+ const modelChoice = document.querySelector('input[name="model_choice"]:checked').value;
119
+ formData.append('model_choice', modelChoice);
120
+ if (modelChoice === 'movenet') {
121
+ const movenetVariant = document.querySelector('input[name="movenet_variant"]:checked').value;
122
+ formData.append('movenet_variant', movenetVariant);
123
+ }
124
+
125
+ // Show loading
126
+ document.getElementById('loading').classList.remove('hidden');
127
+ document.getElementById('result').classList.add('hidden');
128
+
129
+ try {
130
+ const response = await fetch('/upload', {
131
+ method: 'POST',
132
+ body: formData
133
+ });
134
+
135
+ console.log('[CLIENT] Full response object from /upload:', response);
136
+ console.log('[CLIENT] Response status from /upload:', response.status);
137
+ console.log('[CLIENT] Response status text from /upload:', response.statusText);
138
+
139
+ const data = await response.json();
140
+ console.log('[CLIENT] Parsed JSON data from /upload:', data);
141
+
142
+ if (!response.ok) {
143
+ throw new Error(data.error || 'Failed to process video');
144
+ }
145
+
146
+ // Create video element if it doesn't exist
147
+ let videoElement = document.getElementById('outputVideo');
148
+ if (!videoElement) {
149
+ videoElement = document.createElement('video');
150
+ videoElement.id = 'outputVideo';
151
+ videoElement.controls = true;
152
+ videoElement.style.width = '100%';
153
+ videoElement.style.maxWidth = '800px';
154
+ document.getElementById('result').appendChild(videoElement);
155
+ }
156
+
157
+ // Set up video source
158
+ videoElement.src = data.output_path;
159
+
160
+ // Wait for video to be loaded
161
+ await new Promise((resolve, reject) => {
162
+ videoElement.onloadeddata = resolve;
163
+ videoElement.onerror = reject;
164
+ videoElement.load();
165
+ });
166
+
167
+ // Show result
168
+ document.getElementById('loading').classList.add('hidden');
169
+ document.getElementById('result').classList.remove('hidden');
170
+
171
+ } catch (error) {
172
+ console.error('[CLIENT] Error:', error);
173
+ document.getElementById('loading').classList.add('hidden');
174
+ alert('Error processing video: ' + error.message);
175
+ }
176
+ });
177
+ </script>
178
+ </body>
179
+ </html>