Upload app.py
Browse files
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 |
-
#
|
| 19 |
# ------------------------------------------------------------------
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
#
|
| 26 |
-
|
| 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":
|
| 56 |
-
"relevant_score":
|
| 57 |
"body_part_scores": {
|
| 58 |
-
"core":
|
| 59 |
-
"right_arm":
|
| 60 |
-
"left_arm":
|
| 61 |
-
"torso":
|
| 62 |
},
|
| 63 |
"relevant_body_part_scores": {
|
| 64 |
-
"core":
|
| 65 |
-
"right_arm":
|
| 66 |
-
"left_arm":
|
| 67 |
-
"torso":
|
| 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":
|
| 78 |
}
|
| 79 |
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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}
|
| 137 |
MODELS_CACHE[persona_name] = None
|
| 138 |
|
| 139 |
-
# Load all models
|
| 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
|
| 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"β οΈ
|
| 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"
|
| 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"
|
| 193 |
|
| 194 |
try:
|
| 195 |
-
#
|
| 196 |
results = score_exercise(
|
| 197 |
user_video_path=video_file,
|
| 198 |
reference_id="pushup",
|
| 199 |
use_dtw=True
|
| 200 |
)
|
| 201 |
|
| 202 |
-
#
|
| 203 |
score = results.get('overall_score', 0)
|
| 204 |
relevant_score = results.get('relevant_score', score)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
# Build body part scores string
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
|
|
|
| 210 |
|
| 211 |
-
#
|
| 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 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
-
Exercise: Push-up
|
| 219 |
Overall Score: {score:.1f}/100
|
| 220 |
-
|
| 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 |
-
|
| 227 |
{feedback_str}
|
| 228 |
"""
|
| 229 |
|
| 230 |
-
# Generate
|
| 231 |
coach_feedback = generate_feedback(persona_choice, report)
|
| 232 |
|
| 233 |
-
# Clean JSON
|
| 234 |
clean_results = {
|
| 235 |
-
"overall_score":
|
| 236 |
-
"relevant_score":
|
| 237 |
-
"body_part_scores": {k:
|
| 238 |
-
"exercise_type":
|
| 239 |
"feedback": scoring_feedback
|
| 240 |
}
|
| 241 |
|
| 242 |
return report, coach_feedback, json.dumps(clean_results, indent=2)
|
| 243 |
|
| 244 |
except Exception as e:
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
return
|
|
|
|
| 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**
|
|
|
|
|
|
|
| 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
|
| 266 |
)
|
| 267 |
|
| 268 |
gr.Markdown("""
|
| 269 |
-
**Coach
|
| 270 |
- π₯ **Hype Beast**: High energy motivation
|
| 271 |
- π **Data Scientist**: Technical analysis
|
| 272 |
-
- πͺ **No-Nonsense Pro**:
|
| 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'
|
| 283 |
)
|
| 284 |
feedback_output = gr.Textbox(
|
| 285 |
label="π¬ Coach Feedback",
|
| 286 |
lines=10,
|
| 287 |
-
placeholder="
|
| 288 |
)
|
| 289 |
|
| 290 |
-
with gr.Accordion("π Raw JSON
|
| 291 |
json_output = gr.Textbox(
|
| 292 |
label="JSON Results",
|
| 293 |
-
lines=
|
| 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 |
-
###
|
| 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 |
-
|
|
|
|
| 313 |
""")
|
| 314 |
|
| 315 |
-
# Launch
|
| 316 |
if __name__ == "__main__":
|
| 317 |
-
demo.launch(
|
|
|
|
| 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()
|