Spaces:
Sleeping
Sleeping
Update main.py
Browse files
main.py
CHANGED
|
@@ -9,6 +9,7 @@ import asyncio
|
|
| 9 |
from fastapi import FastAPI, HTTPException
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
from pydantic import BaseModel
|
|
|
|
| 12 |
from models.model import (
|
| 13 |
load_models,
|
| 14 |
run_inference,
|
|
@@ -56,8 +57,6 @@ app.add_middleware(
|
|
| 56 |
|
| 57 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
# REQUEST / RESPONSE SCHEMAS
|
| 59 |
-
# Pydantic models define exactly what SwiftUI
|
| 60 |
-
# sends and receives β easy to match in Swift
|
| 61 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 62 |
|
| 63 |
class UserProfile(BaseModel):
|
|
@@ -66,8 +65,11 @@ class UserProfile(BaseModel):
|
|
| 66 |
|
| 67 |
|
| 68 |
class PredictRequest(BaseModel):
|
| 69 |
-
text:
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
|
| 73 |
class LabelResult(BaseModel):
|
|
@@ -90,45 +92,51 @@ class MentalOutput(BaseModel):
|
|
| 90 |
|
| 91 |
|
| 92 |
class PredictResponse(BaseModel):
|
| 93 |
-
physical: PhysicalOutput
|
| 94 |
-
mental: MentalOutput
|
| 95 |
-
coach_response: str
|
| 96 |
-
enriched_text: str
|
| 97 |
|
| 98 |
|
| 99 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 100 |
# PROMPT BUILDER
|
| 101 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 102 |
|
| 103 |
-
def build_claude_prompt(goal, modifiers, physical, mental, planned_workout):
|
| 104 |
-
planned_section =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
return f"""
|
| 107 |
You are a personal fitness coach reviewing a completed training session with your athlete.
|
| 108 |
|
| 109 |
-
User goal
|
| 110 |
-
Constraints
|
|
|
|
|
|
|
| 111 |
{planned_section}
|
| 112 |
|
| 113 |
Physical state:
|
| 114 |
-
- Pain
|
| 115 |
-
- Completion
|
| 116 |
-
- Fatigue
|
| 117 |
-
- Recovery need: {physical['recovery_need_label']['label']}
|
| 118 |
|
| 119 |
Mental state:
|
| 120 |
-
- Performance
|
| 121 |
-
- Satisfaction: {mental['satisfaction_label']['label']}
|
| 122 |
-
- PR achieved
|
| 123 |
-
- Motivation
|
| 124 |
-
|
| 125 |
-
REASONING RULES:
|
| 126 |
-
1. If no planned session was provided, skip the completion comparison entirely.
|
| 127 |
-
Do not reference or imply there was a plan. Focus only on what the labels
|
| 128 |
reveal about how the session went.
|
| 129 |
2. PR handling: if pr_achieved is true, open with a genuine, specific celebration tied to the user's goal β not just "great job." Explain what this PR signals about their progress.
|
| 130 |
3. Recovery routing:
|
| 131 |
-
- recovery_need = rest_day
|
| 132 |
- recovery_need = light_day β suggest one low-intensity option only (walk, yoga, or easy swim).
|
| 133 |
- recovery_need = normal or ready β give a forward-looking tip about the next session.
|
| 134 |
4. Pain flag: if pain label is moderate or high, add a one-line note recommending the user monitor that area and consider professional advice if it persists.
|
|
@@ -153,33 +161,36 @@ async def predict(request: PredictRequest):
|
|
| 153 |
Main endpoint β called by SwiftUI after user submits post-workout summary.
|
| 154 |
|
| 155 |
Flow:
|
| 156 |
-
1. Enriches text with goal
|
| 157 |
-
2. Runs physical BERT
|
| 158 |
-
3.
|
| 159 |
-
4.
|
| 160 |
-
5.
|
| 161 |
-
6. Returns classifications + coaching response to SwiftUI
|
| 162 |
"""
|
| 163 |
try:
|
| 164 |
-
# 1. Enrich text with goal
|
|
|
|
|
|
|
| 165 |
enriched_text = f"Goal: {request.user_profile.primary_goal}. {request.text}"
|
| 166 |
|
| 167 |
-
# 2. Run physical and mental
|
| 168 |
physical_output, mental_output = await asyncio.gather(
|
| 169 |
asyncio.to_thread(run_inference, ml_models["physical_model"], ml_models["tokenizer"], enriched_text, PHYSICAL_LABEL_COLS, PHYSICAL_DECODERS),
|
| 170 |
asyncio.to_thread(run_inference, ml_models["mental_model"], ml_models["tokenizer"], enriched_text, MENTAL_LABEL_COLS, MENTAL_DECODERS)
|
| 171 |
)
|
| 172 |
|
| 173 |
-
# 3. Build Claude prompt
|
| 174 |
prompt = build_claude_prompt(
|
| 175 |
request.user_profile.primary_goal,
|
| 176 |
request.user_profile.modifiers,
|
|
|
|
|
|
|
| 177 |
physical_output,
|
| 178 |
-
mental_output
|
|
|
|
| 179 |
)
|
| 180 |
|
| 181 |
# 4. Call Claude API
|
| 182 |
-
# ANTHROPIC_API_KEY must be set as a HuggingFace Space secret
|
| 183 |
claude_client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
|
| 184 |
message = claude_client.messages.create(
|
| 185 |
model="claude-sonnet-4-20250514",
|
|
|
|
| 9 |
from fastapi import FastAPI, HTTPException
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
from pydantic import BaseModel
|
| 12 |
+
from typing import Optional
|
| 13 |
from models.model import (
|
| 14 |
load_models,
|
| 15 |
run_inference,
|
|
|
|
| 57 |
|
| 58 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 59 |
# REQUEST / RESPONSE SCHEMAS
|
|
|
|
|
|
|
| 60 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 61 |
|
| 62 |
class UserProfile(BaseModel):
|
|
|
|
| 65 |
|
| 66 |
|
| 67 |
class PredictRequest(BaseModel):
|
| 68 |
+
text: str # raw user post-workout text β passed to BERT only
|
| 69 |
+
duration_minutes: int # actual session duration
|
| 70 |
+
workout_type: str # e.g. "strength", "cardio", "mobility"
|
| 71 |
+
user_profile: UserProfile
|
| 72 |
+
planned_workout: Optional[str] = None # Claude output from pre-workout coach, if used
|
| 73 |
|
| 74 |
|
| 75 |
class LabelResult(BaseModel):
|
|
|
|
| 92 |
|
| 93 |
|
| 94 |
class PredictResponse(BaseModel):
|
| 95 |
+
physical: PhysicalOutput
|
| 96 |
+
mental: MentalOutput
|
| 97 |
+
coach_response: str # Claude's workout summary + tips
|
| 98 |
+
enriched_text: str # text sent to BERT (for debugging)
|
| 99 |
|
| 100 |
|
| 101 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 102 |
# PROMPT BUILDER
|
| 103 |
# βββββββββββββββββββββββββββββββββββββββββββββ
|
| 104 |
|
| 105 |
+
def build_claude_prompt(goal, modifiers, duration_minutes, workout_type, physical, mental, planned_workout):
|
| 106 |
+
planned_section = (
|
| 107 |
+
f"Planned session: {planned_workout}"
|
| 108 |
+
if planned_workout
|
| 109 |
+
else "Planned session: none β user worked out independently"
|
| 110 |
+
)
|
| 111 |
|
| 112 |
return f"""
|
| 113 |
You are a personal fitness coach reviewing a completed training session with your athlete.
|
| 114 |
|
| 115 |
+
User goal : {goal}
|
| 116 |
+
Constraints : {', '.join(modifiers) if modifiers else 'none'}
|
| 117 |
+
Session duration: {duration_minutes} minutes
|
| 118 |
+
Workout type : {workout_type}
|
| 119 |
{planned_section}
|
| 120 |
|
| 121 |
Physical state:
|
| 122 |
+
- Pain : {physical['pain_label']['label']}
|
| 123 |
+
- Completion : {physical['completion_label']['label']}
|
| 124 |
+
- Fatigue : {physical['fatigue_label']['label']}
|
| 125 |
+
- Recovery need : {physical['recovery_need_label']['label']}
|
| 126 |
|
| 127 |
Mental state:
|
| 128 |
+
- Performance : {mental['performance_label']['label']}
|
| 129 |
+
- Satisfaction : {mental['satisfaction_label']['label']}
|
| 130 |
+
- PR achieved : {mental['pr_achieved_label']['label']}
|
| 131 |
+
- Motivation : {mental['motivation_label']['label']}
|
| 132 |
+
|
| 133 |
+
REASONING RULES β apply before writing:
|
| 134 |
+
1. If no planned session was provided, skip the completion comparison entirely.
|
| 135 |
+
Do not reference or imply there was a plan. Focus only on what the labels
|
| 136 |
reveal about how the session went.
|
| 137 |
2. PR handling: if pr_achieved is true, open with a genuine, specific celebration tied to the user's goal β not just "great job." Explain what this PR signals about their progress.
|
| 138 |
3. Recovery routing:
|
| 139 |
+
- recovery_need = rest_day β tomorrow's advice must be strictly non-training: sleep, nutrition, or mobility only. Do not suggest any workout.
|
| 140 |
- recovery_need = light_day β suggest one low-intensity option only (walk, yoga, or easy swim).
|
| 141 |
- recovery_need = normal or ready β give a forward-looking tip about the next session.
|
| 142 |
4. Pain flag: if pain label is moderate or high, add a one-line note recommending the user monitor that area and consider professional advice if it persists.
|
|
|
|
| 161 |
Main endpoint β called by SwiftUI after user submits post-workout summary.
|
| 162 |
|
| 163 |
Flow:
|
| 164 |
+
1. Enriches user text with goal (for BERT only)
|
| 165 |
+
2. Runs physical and mental BERT models concurrently
|
| 166 |
+
3. Builds Claude prompt from labels + session metadata + optional planned workout
|
| 167 |
+
4. Calls Claude API
|
| 168 |
+
5. Returns classifications + coaching response to SwiftUI
|
|
|
|
| 169 |
"""
|
| 170 |
try:
|
| 171 |
+
# 1. Enrich text with goal for BERT β duration, workout_type, and
|
| 172 |
+
# planned_workout are kept separate so they don't pollute the BERT
|
| 173 |
+
# input distribution
|
| 174 |
enriched_text = f"Goal: {request.user_profile.primary_goal}. {request.text}"
|
| 175 |
|
| 176 |
+
# 2. Run physical and mental models concurrently
|
| 177 |
physical_output, mental_output = await asyncio.gather(
|
| 178 |
asyncio.to_thread(run_inference, ml_models["physical_model"], ml_models["tokenizer"], enriched_text, PHYSICAL_LABEL_COLS, PHYSICAL_DECODERS),
|
| 179 |
asyncio.to_thread(run_inference, ml_models["mental_model"], ml_models["tokenizer"], enriched_text, MENTAL_LABEL_COLS, MENTAL_DECODERS)
|
| 180 |
)
|
| 181 |
|
| 182 |
+
# 3. Build Claude prompt with all available context
|
| 183 |
prompt = build_claude_prompt(
|
| 184 |
request.user_profile.primary_goal,
|
| 185 |
request.user_profile.modifiers,
|
| 186 |
+
request.duration_minutes,
|
| 187 |
+
request.workout_type,
|
| 188 |
physical_output,
|
| 189 |
+
mental_output,
|
| 190 |
+
request.planned_workout
|
| 191 |
)
|
| 192 |
|
| 193 |
# 4. Call Claude API
|
|
|
|
| 194 |
claude_client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
|
| 195 |
message = claude_client.messages.create(
|
| 196 |
model="claude-sonnet-4-20250514",
|