|
|
""" |
|
|
AI Fitness Coach - Hugging Face Spaces Demo |
|
|
Fine-tuned persona-based feedback system |
|
|
""" |
|
|
|
|
|
import gradio as gr |
|
|
import torch |
|
|
import json |
|
|
import numpy as np |
|
|
from pathlib import Path |
|
|
from typing import Tuple |
|
|
from transformers import AutoTokenizer, AutoModelForCausalLM |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def mock_score_exercise(user_video_path, reference_id="pushup", use_dtw=True): |
|
|
""" |
|
|
Mock scoring function that returns realistic demo results. |
|
|
Used when the full pose estimation backend is not available. |
|
|
""" |
|
|
|
|
|
base_score = 72 + np.random.randint(-5, 15) |
|
|
|
|
|
return { |
|
|
"overall_score": float(base_score), |
|
|
"relevant_score": float(base_score + np.random.randint(-3, 5)), |
|
|
"body_part_scores": { |
|
|
"core": float(base_score + np.random.randint(-8, 10)), |
|
|
"right_arm": float(base_score + np.random.randint(-10, 8)), |
|
|
"left_arm": float(base_score + np.random.randint(-10, 8)), |
|
|
"torso": float(base_score + np.random.randint(-5, 12)) |
|
|
}, |
|
|
"relevant_body_part_scores": { |
|
|
"core": float(base_score + np.random.randint(-8, 10)), |
|
|
"right_arm": float(base_score + np.random.randint(-10, 8)), |
|
|
"left_arm": float(base_score + np.random.randint(-10, 8)), |
|
|
"torso": float(base_score + np.random.randint(-5, 12)) |
|
|
}, |
|
|
"feedback": generate_mock_feedback(base_score), |
|
|
"exercise_type": "pushup", |
|
|
"num_frames_user": 100, |
|
|
"num_frames_ref": 325, |
|
|
"alignment_quality": float(80 + np.random.randint(-10, 15)) |
|
|
} |
|
|
|
|
|
def generate_mock_feedback(score): |
|
|
"""Generate appropriate feedback based on score.""" |
|
|
feedback = [] |
|
|
|
|
|
if score >= 85: |
|
|
feedback.append("Excellent form! Your push-up technique is very close to ideal.") |
|
|
feedback.append("Maintain this consistency in your workouts.") |
|
|
elif score >= 70: |
|
|
feedback.append("Good form overall. Minor adjustments can improve your technique.") |
|
|
feedback.append("Focus on keeping your core engaged throughout the movement.") |
|
|
elif score >= 55: |
|
|
feedback.append("Decent effort, but there's room for improvement.") |
|
|
feedback.append("Try to maintain a straighter back during the movement.") |
|
|
feedback.append("Your arm positioning could be more consistent.") |
|
|
else: |
|
|
feedback.append("Keep practicing! Focus on the basics of proper form.") |
|
|
feedback.append("Watch the reference video and pay attention to body alignment.") |
|
|
feedback.append("Consider starting with modified push-ups to build strength.") |
|
|
|
|
|
return feedback |
|
|
|
|
|
|
|
|
score_exercise = mock_score_exercise |
|
|
print("βΉοΈ Using demonstration scoring mode.") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
MODEL_CONFIG = { |
|
|
"Hype Beast π₯": "rlogh/fitness-coach-persona-hype-beast", |
|
|
"Data Scientist π": "rlogh/fitness-coach-persona-data-scientist", |
|
|
"No-Nonsense Pro πͺ": "rlogh/fitness-coach-persona-no-nonsense-pro", |
|
|
"Mindful Aligner π§": "rlogh/fitness-coach-persona-mindful-aligner", |
|
|
} |
|
|
|
|
|
PERSONAS = list(MODEL_CONFIG.keys()) |
|
|
MODELS_CACHE = {} |
|
|
|
|
|
def load_all_models(): |
|
|
"""Loads all fine-tuned models into the cache on startup.""" |
|
|
global MODELS_CACHE |
|
|
|
|
|
BASE_MODEL_NAME = "distilgpt2" |
|
|
print(f"π Loading base tokenizer from {BASE_MODEL_NAME}...") |
|
|
try: |
|
|
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_NAME, use_fast=True) |
|
|
MODELS_CACHE['tokenizer'] = tokenizer |
|
|
print("β
Base tokenizer loaded successfully.") |
|
|
except Exception as e: |
|
|
print(f"β Critical Error loading tokenizer: {e}") |
|
|
return |
|
|
|
|
|
for persona_name, repo_id in MODEL_CONFIG.items(): |
|
|
print(f"π Loading {persona_name} model from {repo_id}...") |
|
|
try: |
|
|
model = AutoModelForCausalLM.from_pretrained( |
|
|
repo_id, |
|
|
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32, |
|
|
device_map="auto" if torch.cuda.is_available() else None, |
|
|
low_cpu_mem_usage=True, |
|
|
trust_remote_code=True |
|
|
) |
|
|
MODELS_CACHE[persona_name] = model |
|
|
print(f"β
{persona_name} loaded successfully.") |
|
|
except Exception as e: |
|
|
print(f"β Failed to load {persona_name}: {e}") |
|
|
MODELS_CACHE[persona_name] = None |
|
|
|
|
|
|
|
|
load_all_models() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_feedback(persona_name: str, input_report: str) -> str: |
|
|
"""Generates feedback using the selected persona model.""" |
|
|
|
|
|
model = MODELS_CACHE.get(persona_name) |
|
|
tokenizer = MODELS_CACHE.get('tokenizer') |
|
|
|
|
|
if model is None or tokenizer is None: |
|
|
return f"β οΈ The '{persona_name}' coach is currently unavailable. Please try another coach." |
|
|
|
|
|
prompt = f"<|persona|>{persona_name}<|input|>{input_report}<|output|>" |
|
|
|
|
|
try: |
|
|
inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=512) |
|
|
|
|
|
device = next(model.parameters()).device |
|
|
inputs = {k: v.to(device) for k, v in inputs.items()} |
|
|
|
|
|
with torch.no_grad(): |
|
|
outputs = model.generate( |
|
|
**inputs, |
|
|
max_new_tokens=300, |
|
|
temperature=0.9, |
|
|
top_p=0.92, |
|
|
do_sample=True, |
|
|
repetition_penalty=1.2, |
|
|
pad_token_id=tokenizer.eos_token_id |
|
|
) |
|
|
|
|
|
full_text = tokenizer.decode(outputs[0], skip_special_tokens=True) |
|
|
|
|
|
if "<|output|>" in full_text: |
|
|
return full_text.split("<|output|>")[-1].strip() |
|
|
return full_text |
|
|
|
|
|
except Exception as e: |
|
|
return f"Coach feedback error: {str(e)}" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def analyze_video(video_file, persona_choice: str) -> Tuple[str, str, str]: |
|
|
"""Analyze video and return technical report, coach feedback, and JSON results.""" |
|
|
|
|
|
if video_file is None: |
|
|
return "β οΈ Please upload a video first.", "", "{}" |
|
|
|
|
|
if MODELS_CACHE.get(persona_choice) is None: |
|
|
return f"β οΈ The '{persona_choice}' coach failed to load. Try another coach.", "", "{}" |
|
|
|
|
|
try: |
|
|
|
|
|
results = score_exercise( |
|
|
user_video_path=video_file, |
|
|
reference_id="pushup", |
|
|
use_dtw=True |
|
|
) |
|
|
|
|
|
|
|
|
score = results.get('overall_score', 0) |
|
|
relevant_score = results.get('relevant_score', score) |
|
|
body_scores = results.get('relevant_body_part_scores', results.get('body_part_scores', {})) |
|
|
scoring_feedback = results.get('feedback', []) |
|
|
|
|
|
|
|
|
score = max(0, min(100, score)) |
|
|
relevant_score = max(0, min(100, relevant_score)) |
|
|
body_scores = {k: max(0, min(100, v)) for k, v in body_scores.items()} |
|
|
|
|
|
|
|
|
body_parts_str = "\n".join([ |
|
|
f" β’ {part.replace('_', ' ').title()}: {s:.1f}/100" |
|
|
for part, s in body_scores.items() |
|
|
]) |
|
|
|
|
|
|
|
|
feedback_str = "\n".join([f" β’ {fb}" for fb in scoring_feedback]) if scoring_feedback else " β’ Good effort!" |
|
|
|
|
|
|
|
|
if score >= 85: |
|
|
rating = "π Excellent!" |
|
|
elif score >= 70: |
|
|
rating = "π Good" |
|
|
elif score >= 55: |
|
|
rating = "πͺ Keep Practicing" |
|
|
else: |
|
|
rating = "π Review Form" |
|
|
|
|
|
|
|
|
report = f"""π PUSH-UP ANALYSIS |
|
|
ββββββββββββββββββββββββ |
|
|
|
|
|
Overall Score: {score:.1f}/100 |
|
|
Rating: {rating} |
|
|
|
|
|
Body Part Breakdown: |
|
|
{body_parts_str} |
|
|
|
|
|
Observations: |
|
|
{feedback_str} |
|
|
""" |
|
|
|
|
|
|
|
|
coach_feedback = generate_feedback(persona_choice, report) |
|
|
|
|
|
|
|
|
clean_results = { |
|
|
"overall_score": round(score, 1), |
|
|
"relevant_score": round(relevant_score, 1), |
|
|
"body_part_scores": {k: round(v, 1) for k, v in body_scores.items()}, |
|
|
"exercise_type": "pushup", |
|
|
"feedback": scoring_feedback |
|
|
} |
|
|
|
|
|
return report, coach_feedback, json.dumps(clean_results, indent=2) |
|
|
|
|
|
except Exception as e: |
|
|
error_msg = f"Analysis error: {str(e)}" |
|
|
print(f"β {error_msg}") |
|
|
return error_msg, "", "{}" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks(title="AI Fitness Coach", theme=gr.themes.Soft()) as demo: |
|
|
gr.Markdown(""" |
|
|
# ποΈ AI Fitness Coach |
|
|
|
|
|
Upload a video of your **push-up** and get personalized feedback from our AI coaches! |
|
|
|
|
|
> **Note:** This is a demonstration using simulated scoring. The AI coach feedback is generated by fine-tuned language models. |
|
|
""") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
video_input = gr.Video(label="πΉ Upload Your Push-up Video") |
|
|
persona_select = gr.Radio( |
|
|
choices=PERSONAS, |
|
|
value=PERSONAS[0], |
|
|
label="π Choose Your Coach" |
|
|
) |
|
|
|
|
|
gr.Markdown(""" |
|
|
**Coach Styles:** |
|
|
- π₯ **Hype Beast**: High energy motivation |
|
|
- π **Data Scientist**: Technical analysis |
|
|
- πͺ **No-Nonsense Pro**: Direct feedback |
|
|
- π§ **Mindful Aligner**: Balanced approach |
|
|
""") |
|
|
|
|
|
analyze_btn = gr.Button("π― Analyze My Form", variant="primary", size="lg") |
|
|
|
|
|
with gr.Column(scale=2): |
|
|
report_output = gr.Textbox( |
|
|
label="π Technical Analysis", |
|
|
lines=12, |
|
|
placeholder="Upload a video and click 'Analyze My Form'..." |
|
|
) |
|
|
feedback_output = gr.Textbox( |
|
|
label="π¬ Coach Feedback", |
|
|
lines=10, |
|
|
placeholder="Your personalized coaching feedback will appear here..." |
|
|
) |
|
|
|
|
|
with gr.Accordion("π Raw Data (JSON)", open=False): |
|
|
json_output = gr.Textbox( |
|
|
label="JSON Results", |
|
|
lines=6 |
|
|
) |
|
|
|
|
|
|
|
|
analyze_btn.click( |
|
|
fn=analyze_video, |
|
|
inputs=[video_input, persona_select], |
|
|
outputs=[report_output, feedback_output, json_output], |
|
|
api_name=False |
|
|
) |
|
|
|
|
|
gr.Markdown(""" |
|
|
--- |
|
|
### About |
|
|
|
|
|
This app uses **4 fine-tuned DistilGPT-2 models**, each trained with a unique coaching personality. |
|
|
Upload any push-up video to receive personalized form feedback! |
|
|
""") |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch() |
|
|
|