rlogh commited on
Commit
993887f
Β·
verified Β·
1 Parent(s): b7cd405

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +109 -116
app.py CHANGED
@@ -5,92 +5,69 @@ Fine-tuned persona-based feedback system
5
 
6
  import gradio as gr
7
  import torch
8
- import os
9
  import json
10
- import tempfile
11
  import numpy as np
12
- import sys
13
  from pathlib import Path
14
  from typing import Tuple
15
  from transformers import AutoTokenizer, AutoModelForCausalLM
16
 
17
  # ------------------------------------------------------------------
18
- # Configuration
19
  # ------------------------------------------------------------------
20
- PROJECT_ROOT = Path(__file__).parent
21
- sys.path.insert(0, str(PROJECT_ROOT))
22
-
23
- # ------------------------------------------------------------------
24
- # Reference Data Check
25
- # ------------------------------------------------------------------
26
- def check_reference_data():
27
- """Check if reference data files exist locally."""
28
- ref_dir = PROJECT_ROOT / "references" / "pushup"
29
- required_files = ["keypoints_3D.npz", "metadata.json", "noisy_samples.npz", "statistical_bounds.npz"]
30
 
31
- # Check if all files exist
32
- missing = [f for f in required_files if not (ref_dir / f).exists()]
33
-
34
- if not missing:
35
- print("βœ… Reference data found locally.")
36
- return True
37
-
38
- print(f"⚠️ Reference data not found. Missing files:")
39
- for f in missing:
40
- print(f" - references/pushup/{f}")
41
- print("\nπŸ“‹ To fix: Add the 'references/pushup/' folder to your Space repository")
42
- print(" with these files: keypoints_3D.npz, metadata.json, noisy_samples.npz, statistical_bounds.npz")
43
- print("\nπŸ”„ Using mock scoring for now...")
44
- return False
45
-
46
- # Try to load reference data
47
- REFERENCE_DATA_AVAILABLE = check_reference_data()
48
-
49
- # ------------------------------------------------------------------
50
- # Import backend logic (with error handling if modules are missing)
51
- # ------------------------------------------------------------------
52
- def mock_score_exercise(user_video_path, reference_id, use_dtw):
53
- """Mock function for testing without the full backend logic."""
54
  return {
55
- "overall_score": 75.5,
56
- "relevant_score": 78.0,
57
  "body_part_scores": {
58
- "core": 80.0,
59
- "right_arm": 75.0,
60
- "left_arm": 76.0,
61
- "torso": 77.0
62
  },
63
  "relevant_body_part_scores": {
64
- "core": 80.0,
65
- "right_arm": 75.0,
66
- "left_arm": 76.0,
67
- "torso": 77.0
68
  },
69
- "feedback": [
70
- "Good form overall. Minor adjustments can improve your technique.",
71
- "Keep your core engaged and back straight throughout the movement.",
72
- "Focus on maintaining consistent arm positioning."
73
- ],
74
  "exercise_type": "pushup",
75
  "num_frames_user": 100,
76
  "num_frames_ref": 325,
77
- "alignment_quality": 85
78
  }
79
 
80
- # Try to import the real scoring function
81
- score_exercise = None
82
- if REFERENCE_DATA_AVAILABLE:
83
- try:
84
- from fitness_coach.comparison import score_exercise as real_score_exercise
85
- score_exercise = real_score_exercise
86
- print("βœ… Full scoring backend loaded.")
87
- except ImportError as e:
88
- print(f"⚠️ Could not import fitness_coach: {e}")
89
- print(" Using mock scoring.")
90
- score_exercise = mock_score_exercise
91
- else:
92
- score_exercise = mock_score_exercise
93
- print("⚠️ Using mock scoring (reference data not available).")
 
 
 
 
 
 
 
 
 
 
94
 
95
  # ------------------------------------------------------------------
96
  # Model Loading Logic
@@ -110,13 +87,13 @@ def load_all_models():
110
  global MODELS_CACHE
111
 
112
  BASE_MODEL_NAME = "distilgpt2"
113
- print(f"πŸ”„ Loading base tokenizer directly from {BASE_MODEL_NAME}...")
114
  try:
115
  tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_NAME, use_fast=True)
116
  MODELS_CACHE['tokenizer'] = tokenizer
117
  print("βœ… Base tokenizer loaded successfully.")
118
  except Exception as e:
119
- print(f"❌ Critical Error loading tokenizer from {BASE_MODEL_NAME}: {e}")
120
  return
121
 
122
  for persona_name, repo_id in MODEL_CONFIG.items():
@@ -131,25 +108,24 @@ def load_all_models():
131
  )
132
  MODELS_CACHE[persona_name] = model
133
  print(f"βœ… {persona_name} loaded successfully.")
134
-
135
  except Exception as e:
136
- print(f"❌ Failed to load {persona_name} from {repo_id}: {e}")
137
  MODELS_CACHE[persona_name] = None
138
 
139
- # Load all models immediately on startup
140
  load_all_models()
141
 
142
  # ------------------------------------------------------------------
143
  # Feedback Generation
144
  # ------------------------------------------------------------------
145
  def generate_feedback(persona_name: str, input_report: str) -> str:
146
- """Generates feedback using the selected model and prompt structure."""
147
 
148
  model = MODELS_CACHE.get(persona_name)
149
  tokenizer = MODELS_CACHE.get('tokenizer')
150
 
151
  if model is None or tokenizer is None:
152
- return f"⚠️ Error: The '{persona_name}' AI Model failed to load. Check Space logs."
153
 
154
  prompt = f"<|persona|>{persona_name}<|input|>{input_report}<|output|>"
155
 
@@ -177,7 +153,7 @@ def generate_feedback(persona_name: str, input_report: str) -> str:
177
  return full_text
178
 
179
  except Exception as e:
180
- return f"Generation Error: {str(e)}"
181
 
182
 
183
  # ------------------------------------------------------------------
@@ -185,66 +161,84 @@ def generate_feedback(persona_name: str, input_report: str) -> str:
185
  # ------------------------------------------------------------------
186
  def analyze_video(video_file, persona_choice: str) -> Tuple[str, str, str]:
187
  """Analyze video and return technical report, coach feedback, and JSON results."""
 
188
  if video_file is None:
189
- return "Please upload a video first.", "", "{}"
190
 
191
  if MODELS_CACHE.get(persona_choice) is None:
192
- return f"Error: The model for '{persona_choice}' failed to load during startup.", "", "{}"
193
 
194
  try:
195
- # Always use pushup as reference
196
  results = score_exercise(
197
  user_video_path=video_file,
198
  reference_id="pushup",
199
  use_dtw=True
200
  )
201
 
202
- # Format the technical report
203
  score = results.get('overall_score', 0)
204
  relevant_score = results.get('relevant_score', score)
 
 
 
 
 
 
 
205
 
206
  # Build body part scores string
207
- body_scores = results.get('relevant_body_part_scores', results.get('body_part_scores', {}))
208
- body_parts_str = "\n".join([f" β€’ {part.replace('_', ' ').title()}: {score:.1f}/100"
209
- for part, score in body_scores.items()])
 
210
 
211
- # Get feedback from scoring
212
- scoring_feedback = results.get('feedback', [])
213
  feedback_str = "\n".join([f" β€’ {fb}" for fb in scoring_feedback]) if scoring_feedback else " β€’ Good effort!"
214
 
215
- report = f"""πŸ“Š EXERCISE ANALYSIS SUMMARY
216
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 
 
 
 
 
 
 
 
 
 
 
217
 
218
- Exercise: Push-up
219
  Overall Score: {score:.1f}/100
220
- Relevant Body Score: {relevant_score:.1f}/100
221
- Rating: {"🌟 Excellent!" if score >= 80 else "πŸ‘ Good" if score >= 60 else "πŸ’ͺ Needs Work"}
222
 
223
  Body Part Breakdown:
224
  {body_parts_str}
225
 
226
- Key Observations:
227
  {feedback_str}
228
  """
229
 
230
- # Generate AI Persona feedback
231
  coach_feedback = generate_feedback(persona_choice, report)
232
 
233
- # Clean JSON for output (remove numpy arrays)
234
  clean_results = {
235
- "overall_score": float(results.get('overall_score', 0)),
236
- "relevant_score": float(results.get('relevant_score', 0)),
237
- "body_part_scores": {k: float(v) for k, v in body_scores.items()},
238
- "exercise_type": results.get('exercise_type', 'pushup'),
239
  "feedback": scoring_feedback
240
  }
241
 
242
  return report, coach_feedback, json.dumps(clean_results, indent=2)
243
 
244
  except Exception as e:
245
- import traceback
246
- traceback.print_exc()
247
- return f"Error during analysis: {e}", "", "{}"
 
248
 
249
  # ------------------------------------------------------------------
250
  # Gradio UI
@@ -253,7 +247,9 @@ with gr.Blocks(title="AI Fitness Coach", theme=gr.themes.Soft()) as demo:
253
  gr.Markdown("""
254
  # πŸ‹οΈ AI Fitness Coach
255
 
256
- Upload a video of your **push-up** exercise and get personalized feedback from our AI coaches!
 
 
257
  """)
258
 
259
  with gr.Row():
@@ -262,14 +258,14 @@ with gr.Blocks(title="AI Fitness Coach", theme=gr.themes.Soft()) as demo:
262
  persona_select = gr.Radio(
263
  choices=PERSONAS,
264
  value=PERSONAS[0],
265
- label="🎭 Choose Your Coach Persona"
266
  )
267
 
268
  gr.Markdown("""
269
- **Coach Personalities:**
270
  - πŸ”₯ **Hype Beast**: High energy motivation
271
  - πŸ“Š **Data Scientist**: Technical analysis
272
- - πŸ’ͺ **No-Nonsense Pro**: Straight to the point
273
  - 🧘 **Mindful Aligner**: Balanced approach
274
  """)
275
 
@@ -279,22 +275,21 @@ with gr.Blocks(title="AI Fitness Coach", theme=gr.themes.Soft()) as demo:
279
  report_output = gr.Textbox(
280
  label="πŸ“Š Technical Analysis",
281
  lines=12,
282
- placeholder="Upload a video and click 'Analyze My Form' to see your results..."
283
  )
284
  feedback_output = gr.Textbox(
285
  label="πŸ’¬ Coach Feedback",
286
  lines=10,
287
- placeholder="Personalized coaching feedback will appear here..."
288
  )
289
 
290
- with gr.Accordion("πŸ“‹ Raw JSON Data", open=False):
291
  json_output = gr.Textbox(
292
  label="JSON Results",
293
- lines=8,
294
- placeholder="JSON results will appear here..."
295
  )
296
 
297
- # Connect button
298
  analyze_btn.click(
299
  fn=analyze_video,
300
  inputs=[video_input, persona_select],
@@ -304,14 +299,12 @@ with gr.Blocks(title="AI Fitness Coach", theme=gr.themes.Soft()) as demo:
304
 
305
  gr.Markdown("""
306
  ---
307
- ### πŸ”¬ About This App
308
-
309
- This app uses **4 fine-tuned DistilGPT-2 models** to provide personalized fitness coaching feedback.
310
- Each model has been trained to match a specific coaching personality.
311
 
312
- **Note:** Currently only push-up analysis is supported.
 
313
  """)
314
 
315
- # Launch
316
  if __name__ == "__main__":
317
- demo.launch(share=False)
 
5
 
6
  import gradio as gr
7
  import torch
 
8
  import json
 
9
  import numpy as np
 
10
  from pathlib import Path
11
  from typing import Tuple
12
  from transformers import AutoTokenizer, AutoModelForCausalLM
13
 
14
  # ------------------------------------------------------------------
15
+ # Mock Scoring Function (used when full backend unavailable)
16
  # ------------------------------------------------------------------
17
+ def mock_score_exercise(user_video_path, reference_id="pushup", use_dtw=True):
18
+ """
19
+ Mock scoring function that returns realistic demo results.
20
+ Used when the full pose estimation backend is not available.
21
+ """
22
+ # Generate slightly randomized but realistic scores
23
+ base_score = 72 + np.random.randint(-5, 15)
 
 
 
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  return {
26
+ "overall_score": float(base_score),
27
+ "relevant_score": float(base_score + np.random.randint(-3, 5)),
28
  "body_part_scores": {
29
+ "core": float(base_score + np.random.randint(-8, 10)),
30
+ "right_arm": float(base_score + np.random.randint(-10, 8)),
31
+ "left_arm": float(base_score + np.random.randint(-10, 8)),
32
+ "torso": float(base_score + np.random.randint(-5, 12))
33
  },
34
  "relevant_body_part_scores": {
35
+ "core": float(base_score + np.random.randint(-8, 10)),
36
+ "right_arm": float(base_score + np.random.randint(-10, 8)),
37
+ "left_arm": float(base_score + np.random.randint(-10, 8)),
38
+ "torso": float(base_score + np.random.randint(-5, 12))
39
  },
40
+ "feedback": generate_mock_feedback(base_score),
 
 
 
 
41
  "exercise_type": "pushup",
42
  "num_frames_user": 100,
43
  "num_frames_ref": 325,
44
+ "alignment_quality": float(80 + np.random.randint(-10, 15))
45
  }
46
 
47
+ def generate_mock_feedback(score):
48
+ """Generate appropriate feedback based on score."""
49
+ feedback = []
50
+
51
+ if score >= 85:
52
+ feedback.append("Excellent form! Your push-up technique is very close to ideal.")
53
+ feedback.append("Maintain this consistency in your workouts.")
54
+ elif score >= 70:
55
+ feedback.append("Good form overall. Minor adjustments can improve your technique.")
56
+ feedback.append("Focus on keeping your core engaged throughout the movement.")
57
+ elif score >= 55:
58
+ feedback.append("Decent effort, but there's room for improvement.")
59
+ feedback.append("Try to maintain a straighter back during the movement.")
60
+ feedback.append("Your arm positioning could be more consistent.")
61
+ else:
62
+ feedback.append("Keep practicing! Focus on the basics of proper form.")
63
+ feedback.append("Watch the reference video and pay attention to body alignment.")
64
+ feedback.append("Consider starting with modified push-ups to build strength.")
65
+
66
+ return feedback
67
+
68
+ # Use mock scoring (full backend requires dependencies not available on Spaces)
69
+ score_exercise = mock_score_exercise
70
+ print("ℹ️ Using demonstration scoring mode.")
71
 
72
  # ------------------------------------------------------------------
73
  # Model Loading Logic
 
87
  global MODELS_CACHE
88
 
89
  BASE_MODEL_NAME = "distilgpt2"
90
+ print(f"πŸ”„ Loading base tokenizer from {BASE_MODEL_NAME}...")
91
  try:
92
  tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_NAME, use_fast=True)
93
  MODELS_CACHE['tokenizer'] = tokenizer
94
  print("βœ… Base tokenizer loaded successfully.")
95
  except Exception as e:
96
+ print(f"❌ Critical Error loading tokenizer: {e}")
97
  return
98
 
99
  for persona_name, repo_id in MODEL_CONFIG.items():
 
108
  )
109
  MODELS_CACHE[persona_name] = model
110
  print(f"βœ… {persona_name} loaded successfully.")
 
111
  except Exception as e:
112
+ print(f"❌ Failed to load {persona_name}: {e}")
113
  MODELS_CACHE[persona_name] = None
114
 
115
+ # Load all models on startup
116
  load_all_models()
117
 
118
  # ------------------------------------------------------------------
119
  # Feedback Generation
120
  # ------------------------------------------------------------------
121
  def generate_feedback(persona_name: str, input_report: str) -> str:
122
+ """Generates feedback using the selected persona model."""
123
 
124
  model = MODELS_CACHE.get(persona_name)
125
  tokenizer = MODELS_CACHE.get('tokenizer')
126
 
127
  if model is None or tokenizer is None:
128
+ return f"⚠️ The '{persona_name}' coach is currently unavailable. Please try another coach."
129
 
130
  prompt = f"<|persona|>{persona_name}<|input|>{input_report}<|output|>"
131
 
 
153
  return full_text
154
 
155
  except Exception as e:
156
+ return f"Coach feedback error: {str(e)}"
157
 
158
 
159
  # ------------------------------------------------------------------
 
161
  # ------------------------------------------------------------------
162
  def analyze_video(video_file, persona_choice: str) -> Tuple[str, str, str]:
163
  """Analyze video and return technical report, coach feedback, and JSON results."""
164
+
165
  if video_file is None:
166
+ return "⚠️ Please upload a video first.", "", "{}"
167
 
168
  if MODELS_CACHE.get(persona_choice) is None:
169
+ return f"⚠️ The '{persona_choice}' coach failed to load. Try another coach.", "", "{}"
170
 
171
  try:
172
+ # Score the exercise
173
  results = score_exercise(
174
  user_video_path=video_file,
175
  reference_id="pushup",
176
  use_dtw=True
177
  )
178
 
179
+ # Extract scores
180
  score = results.get('overall_score', 0)
181
  relevant_score = results.get('relevant_score', score)
182
+ body_scores = results.get('relevant_body_part_scores', results.get('body_part_scores', {}))
183
+ scoring_feedback = results.get('feedback', [])
184
+
185
+ # Clamp scores to valid range
186
+ score = max(0, min(100, score))
187
+ relevant_score = max(0, min(100, relevant_score))
188
+ body_scores = {k: max(0, min(100, v)) for k, v in body_scores.items()}
189
 
190
  # Build body part scores string
191
+ body_parts_str = "\n".join([
192
+ f" β€’ {part.replace('_', ' ').title()}: {s:.1f}/100"
193
+ for part, s in body_scores.items()
194
+ ])
195
 
196
+ # Build feedback string
 
197
  feedback_str = "\n".join([f" β€’ {fb}" for fb in scoring_feedback]) if scoring_feedback else " β€’ Good effort!"
198
 
199
+ # Determine rating
200
+ if score >= 85:
201
+ rating = "🌟 Excellent!"
202
+ elif score >= 70:
203
+ rating = "πŸ‘ Good"
204
+ elif score >= 55:
205
+ rating = "πŸ’ͺ Keep Practicing"
206
+ else:
207
+ rating = "πŸ“š Review Form"
208
+
209
+ # Format technical report
210
+ report = f"""πŸ“Š PUSH-UP ANALYSIS
211
+ ━━━━━━━━━━━━━━━━━━━━━━━━
212
 
 
213
  Overall Score: {score:.1f}/100
214
+ Rating: {rating}
 
215
 
216
  Body Part Breakdown:
217
  {body_parts_str}
218
 
219
+ Observations:
220
  {feedback_str}
221
  """
222
 
223
+ # Generate personalized coach feedback
224
  coach_feedback = generate_feedback(persona_choice, report)
225
 
226
+ # Clean JSON output
227
  clean_results = {
228
+ "overall_score": round(score, 1),
229
+ "relevant_score": round(relevant_score, 1),
230
+ "body_part_scores": {k: round(v, 1) for k, v in body_scores.items()},
231
+ "exercise_type": "pushup",
232
  "feedback": scoring_feedback
233
  }
234
 
235
  return report, coach_feedback, json.dumps(clean_results, indent=2)
236
 
237
  except Exception as e:
238
+ error_msg = f"Analysis error: {str(e)}"
239
+ print(f"❌ {error_msg}")
240
+ return error_msg, "", "{}"
241
+
242
 
243
  # ------------------------------------------------------------------
244
  # Gradio UI
 
247
  gr.Markdown("""
248
  # πŸ‹οΈ AI Fitness Coach
249
 
250
+ Upload a video of your **push-up** and get personalized feedback from our AI coaches!
251
+
252
+ > **Note:** This is a demonstration using simulated scoring. The AI coach feedback is generated by fine-tuned language models.
253
  """)
254
 
255
  with gr.Row():
 
258
  persona_select = gr.Radio(
259
  choices=PERSONAS,
260
  value=PERSONAS[0],
261
+ label="🎭 Choose Your Coach"
262
  )
263
 
264
  gr.Markdown("""
265
+ **Coach Styles:**
266
  - πŸ”₯ **Hype Beast**: High energy motivation
267
  - πŸ“Š **Data Scientist**: Technical analysis
268
+ - πŸ’ͺ **No-Nonsense Pro**: Direct feedback
269
  - 🧘 **Mindful Aligner**: Balanced approach
270
  """)
271
 
 
275
  report_output = gr.Textbox(
276
  label="πŸ“Š Technical Analysis",
277
  lines=12,
278
+ placeholder="Upload a video and click 'Analyze My Form'..."
279
  )
280
  feedback_output = gr.Textbox(
281
  label="πŸ’¬ Coach Feedback",
282
  lines=10,
283
+ placeholder="Your personalized coaching feedback will appear here..."
284
  )
285
 
286
+ with gr.Accordion("πŸ“‹ Raw Data (JSON)", open=False):
287
  json_output = gr.Textbox(
288
  label="JSON Results",
289
+ lines=6
 
290
  )
291
 
292
+ # Connect button to function
293
  analyze_btn.click(
294
  fn=analyze_video,
295
  inputs=[video_input, persona_select],
 
299
 
300
  gr.Markdown("""
301
  ---
302
+ ### About
 
 
 
303
 
304
+ This app uses **4 fine-tuned DistilGPT-2 models**, each trained with a unique coaching personality.
305
+ Upload any push-up video to receive personalized form feedback!
306
  """)
307
 
308
+ # Launch the app
309
  if __name__ == "__main__":
310
+ demo.launch()