Spaces:
Running
Running
Commit
·
1810165
1
Parent(s):
bb3dc70
feat: create flask server
Browse files
.gitignore
CHANGED
@@ -1,3 +1,4 @@
|
|
1 |
node_modules/
|
2 |
dist/
|
3 |
-
.env
|
|
|
|
1 |
node_modules/
|
2 |
dist/
|
3 |
+
.env
|
4 |
+
env/
|
models-server/models/__pycache__/fitness_model.cpython-312.pyc
ADDED
Binary file (9.84 kB). View file
|
|
models-server/models/fitness_model.py
ADDED
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pickle
|
2 |
+
import os
|
3 |
+
from sklearn.preprocessing import OneHotEncoder
|
4 |
+
import random
|
5 |
+
import pandas as pd
|
6 |
+
|
7 |
+
SERVER_FILE_DIR = os.path.dirname(os.path.abspath(__file__))
|
8 |
+
FITNESS_MODEL_PATH = os.path.join(
|
9 |
+
SERVER_FILE_DIR, *"../resources/models/fitness_model.pkl".split("/")
|
10 |
+
)
|
11 |
+
|
12 |
+
|
13 |
+
class FitnessModel:
|
14 |
+
def __init__(self, excercise_path, kmeans_path, plan_classifier_path):
|
15 |
+
self.data = pd.read_csv(excercise_path)
|
16 |
+
self.kmeans = None
|
17 |
+
self.plan_classifier = None
|
18 |
+
self.encoder = None
|
19 |
+
self.cluster_data = {}
|
20 |
+
self.X_train_cols = [
|
21 |
+
"level_Advanced",
|
22 |
+
"level_Beginner",
|
23 |
+
"level_Intermediate",
|
24 |
+
"goal_ Get Fitter",
|
25 |
+
"goal_ Lose Weight",
|
26 |
+
"goal_Gain Muscle",
|
27 |
+
"goal_Get Fitter",
|
28 |
+
"goal_Increase Endurance",
|
29 |
+
"goal_Increase Strength",
|
30 |
+
"goal_Sports Performance",
|
31 |
+
"gender_Female",
|
32 |
+
"gender_Male",
|
33 |
+
"gender_Male & Female",
|
34 |
+
]
|
35 |
+
|
36 |
+
# Load kmeans model
|
37 |
+
with open(kmeans_path, "rb") as f:
|
38 |
+
self.kmeans = pickle.load(f)
|
39 |
+
|
40 |
+
# Load plan classifier model
|
41 |
+
with open(plan_classifier_path, "rb") as f:
|
42 |
+
self.plan_classifier = pickle.load(f)
|
43 |
+
|
44 |
+
# Iterate over each cluster label
|
45 |
+
for cluster_label in range(90):
|
46 |
+
# Filter the dataset to get data for the current cluster
|
47 |
+
cluster_subset = self.data[self.data["cluster"] == cluster_label]
|
48 |
+
|
49 |
+
# Add the cluster data to the dictionary
|
50 |
+
self.cluster_data[cluster_label] = cluster_subset
|
51 |
+
|
52 |
+
features = self.data[["Level", "goal", "bodyPart"]]
|
53 |
+
|
54 |
+
# Perform one-hot encoding for categorical features
|
55 |
+
self.encoder = OneHotEncoder(sparse=False)
|
56 |
+
encoded_features = self.encoder.fit_transform(features)
|
57 |
+
|
58 |
+
def choose_plan(self, level, goal, gender):
|
59 |
+
global plan_classifier
|
60 |
+
# Convert input into a DataFrame
|
61 |
+
input_data = pd.DataFrame(
|
62 |
+
{"level": [level], "goal": [goal], "gender": [gender]}
|
63 |
+
)
|
64 |
+
|
65 |
+
# One-hot encode the input data
|
66 |
+
input_encoded = pd.get_dummies(input_data, columns=["level", "goal", "gender"])
|
67 |
+
|
68 |
+
# Ensure that input has the same columns as the model was trained on
|
69 |
+
# This is necessary in case some categories are missing in the input
|
70 |
+
missing_cols = set(self.X_train_cols) - set(input_encoded.columns)
|
71 |
+
for col in missing_cols:
|
72 |
+
input_encoded[col] = 0
|
73 |
+
|
74 |
+
# Reorder columns to match the order of columns in X_train
|
75 |
+
input_encoded = input_encoded[self.X_train_cols]
|
76 |
+
|
77 |
+
# Make prediction for the given input using the trained model
|
78 |
+
prediction = self.plan_classifier.predict(input_encoded)
|
79 |
+
|
80 |
+
# Convert each string in the list to a list of strings
|
81 |
+
daily_activities_lists = [day.split(", ") for day in prediction[0]]
|
82 |
+
|
83 |
+
return daily_activities_lists
|
84 |
+
|
85 |
+
def get_daily_recommendation(self, home_or_gym, level, goal, bodyParts, equipments):
|
86 |
+
if goal in ["Lose Weight", "Get Fitter"]:
|
87 |
+
goal = "Get Fitter & Lose Weight"
|
88 |
+
daily_recommendations = []
|
89 |
+
|
90 |
+
bodyParts = [bp for bp in bodyParts if "-" not in bp]
|
91 |
+
# Repeat elements in bodyParts until it reaches a size of 6
|
92 |
+
while len(bodyParts) < 6:
|
93 |
+
bodyParts += bodyParts
|
94 |
+
|
95 |
+
# Limit bodyParts to size 6
|
96 |
+
bodyParts = bodyParts[:6]
|
97 |
+
|
98 |
+
for bodyPart in bodyParts:
|
99 |
+
# Predict cluster for the specified combination of goal, level, and body part
|
100 |
+
input_data = [[level, goal, bodyPart]]
|
101 |
+
predicted_cluster = self.kmeans.predict(self.encoder.transform(input_data))[
|
102 |
+
0
|
103 |
+
]
|
104 |
+
print(predicted_cluster)
|
105 |
+
# Get data for the predicted cluster
|
106 |
+
cluster_subset = self.cluster_data[predicted_cluster]
|
107 |
+
|
108 |
+
# Filter data based on location (home or gym)
|
109 |
+
if home_or_gym == 0:
|
110 |
+
cluster_subset = cluster_subset[
|
111 |
+
~cluster_subset["equipment"].isin(equipments)
|
112 |
+
]
|
113 |
+
|
114 |
+
# Randomly select one exercise from the cluster if any left after equipment filtering
|
115 |
+
if not cluster_subset.empty:
|
116 |
+
selected_exercise = random.choice(
|
117 |
+
cluster_subset.to_dict(orient="records")
|
118 |
+
)
|
119 |
+
daily_recommendations.append(selected_exercise)
|
120 |
+
|
121 |
+
# Remove duplicates from the list
|
122 |
+
unique_recommendations = []
|
123 |
+
seen_names = set()
|
124 |
+
for exercise in daily_recommendations:
|
125 |
+
if exercise["name"] not in seen_names:
|
126 |
+
unique_recommendations.append(exercise)
|
127 |
+
seen_names.add(exercise["name"])
|
128 |
+
|
129 |
+
return unique_recommendations
|
130 |
+
|
131 |
+
def get_gender_adjustment(self, gender):
|
132 |
+
return 1.0 if gender == "Male" else 0.7
|
133 |
+
|
134 |
+
def get_age_adjustment(self, age):
|
135 |
+
if age < 30:
|
136 |
+
return 1.0
|
137 |
+
elif 30 <= age < 50:
|
138 |
+
return 0.5
|
139 |
+
else:
|
140 |
+
return 0.1
|
141 |
+
|
142 |
+
def get_level_adjustment(self, level):
|
143 |
+
if level == "Beginner":
|
144 |
+
return 0.8
|
145 |
+
elif level == "Intermediate":
|
146 |
+
return 1.0
|
147 |
+
elif level == "Advanced":
|
148 |
+
return 1.2
|
149 |
+
|
150 |
+
def get_body_part_adjustment(self, body_part):
|
151 |
+
body_parts = {
|
152 |
+
"chest": 1,
|
153 |
+
"shoulders": 0.8,
|
154 |
+
"waist": 0.6,
|
155 |
+
"upper legs": 0.7,
|
156 |
+
"back": 0.9,
|
157 |
+
"lower legs": 0.5,
|
158 |
+
"upper arms": 0.8,
|
159 |
+
"cardio": 0.7,
|
160 |
+
"lower arms": 0.6,
|
161 |
+
"neck": 0.5,
|
162 |
+
}
|
163 |
+
return body_parts.get(body_part, 0)
|
164 |
+
|
165 |
+
def adjust_workout(self, gender, age, feedback, body_part, level, old_weight):
|
166 |
+
gender_adjustment = self.get_gender_adjustment(gender)
|
167 |
+
age_adjustment = self.get_age_adjustment(age)
|
168 |
+
level_adjustment = self.get_level_adjustment(level)
|
169 |
+
body_part_adjustment = self.get_body_part_adjustment(body_part)
|
170 |
+
|
171 |
+
increasing_factor_of_weight = (
|
172 |
+
age_adjustment
|
173 |
+
* body_part_adjustment
|
174 |
+
* gender_adjustment
|
175 |
+
* level_adjustment
|
176 |
+
* 0.3
|
177 |
+
)
|
178 |
+
|
179 |
+
if not feedback:
|
180 |
+
increasing_factor_of_weight = (1 - increasing_factor_of_weight) * -0.1
|
181 |
+
|
182 |
+
new_weight = old_weight + increasing_factor_of_weight * old_weight
|
183 |
+
|
184 |
+
return new_weight
|
185 |
+
|
186 |
+
def calculate_new_repetition(self, level, goal):
|
187 |
+
if goal in ["Lose Weight", "Get Fitter"]:
|
188 |
+
if level == "Beginner":
|
189 |
+
return 15
|
190 |
+
elif level == "Intermediate":
|
191 |
+
return 12
|
192 |
+
elif level == "Expert":
|
193 |
+
return 10
|
194 |
+
elif goal == "Gain Muscle":
|
195 |
+
if level == "Beginner":
|
196 |
+
return 10
|
197 |
+
elif level == "Intermediate":
|
198 |
+
return 8
|
199 |
+
elif level == "Advanced":
|
200 |
+
return 6
|
201 |
+
|
202 |
+
def calculate_new_duration(self, level):
|
203 |
+
|
204 |
+
if level == "Beginner":
|
205 |
+
return 20
|
206 |
+
elif level == "Intermediate":
|
207 |
+
return 50
|
208 |
+
elif level == "Advanced":
|
209 |
+
return 80
|
210 |
+
|
211 |
+
def predict(
|
212 |
+
self, home_or_gym, level, goal, gender, age, feedback, old_weight, equipments
|
213 |
+
):
|
214 |
+
|
215 |
+
plan = self.choose_plan(level, goal, gender)
|
216 |
+
print(plan)
|
217 |
+
|
218 |
+
while len(plan) < 30:
|
219 |
+
plan.extend(plan)
|
220 |
+
plan = plan[:30]
|
221 |
+
|
222 |
+
all_recommendations = []
|
223 |
+
for day_body_parts in plan:
|
224 |
+
daily_exercises = self.get_daily_recommendation(
|
225 |
+
home_or_gym, level, goal, day_body_parts, equipments
|
226 |
+
)
|
227 |
+
daily_recommendations = []
|
228 |
+
|
229 |
+
for exercise in daily_exercises:
|
230 |
+
weights = self.adjust_workout(
|
231 |
+
gender, age, feedback, exercise["bodyPart"], level, old_weight
|
232 |
+
)
|
233 |
+
repetitions = self.calculate_new_repetition(level, goal)
|
234 |
+
duration = self.calculate_new_duration(level)
|
235 |
+
weights_or_duration = (
|
236 |
+
weights if exercise["type"] == "weight" else duration
|
237 |
+
)
|
238 |
+
exercise_recommendations = {
|
239 |
+
"name": exercise["name"],
|
240 |
+
"type": exercise["type"],
|
241 |
+
"equipment": exercise["equipment"],
|
242 |
+
"bodyPart": exercise["bodyPart"],
|
243 |
+
"target": exercise["target"],
|
244 |
+
"weights_or_duration": weights_or_duration,
|
245 |
+
"sets": exercise["sets"],
|
246 |
+
"repetitions": repetitions,
|
247 |
+
}
|
248 |
+
daily_recommendations.append(exercise_recommendations)
|
249 |
+
all_recommendations.append(daily_recommendations)
|
250 |
+
|
251 |
+
return all_recommendations # Trim to ensure exactly 30 elements
|
252 |
+
|
253 |
+
|
254 |
+
class FModel:
|
255 |
+
def __init__(self):
|
256 |
+
with open(FITNESS_MODEL_PATH, "rb") as f:
|
257 |
+
self.model = pickle.load(f)
|
258 |
+
|
259 |
+
def predict(
|
260 |
+
self,
|
261 |
+
home_or_gym: int,
|
262 |
+
level: str,
|
263 |
+
goal: str,
|
264 |
+
gender: str,
|
265 |
+
age: int,
|
266 |
+
feedback: bool,
|
267 |
+
old_weight: int,
|
268 |
+
equipments: list,
|
269 |
+
):
|
270 |
+
print("model", self.model)
|
271 |
+
return self.model.predict(
|
272 |
+
home_or_gym, level, goal, gender, age, feedback, old_weight, equipments
|
273 |
+
)
|
models-server/resources/models/fitness_model.pkl
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
version https://git-lfs.github.com/spec/v1
|
2 |
+
oid sha256:665d34c71c506fa1cdbd8d74b54f6ca84f1b9f5a397a6bb90d608cc699f2a61d
|
3 |
+
size 95457799
|
models-server/server.py
ADDED
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, request
|
2 |
+
from dotenv import load_dotenv
|
3 |
+
import os
|
4 |
+
import json
|
5 |
+
from models.fitness_model import FModel
|
6 |
+
|
7 |
+
load_dotenv()
|
8 |
+
|
9 |
+
HOST = os.getenv("MODELS_HOST") or "127.0.0.1"
|
10 |
+
PORT = os.getenv("MODELS_PORT") or "3030"
|
11 |
+
|
12 |
+
fitness_model = FModel()
|
13 |
+
|
14 |
+
app = Flask("model-server")
|
15 |
+
|
16 |
+
|
17 |
+
@app.get("/")
|
18 |
+
def health():
|
19 |
+
return "I'm alive!!"
|
20 |
+
|
21 |
+
|
22 |
+
@app.post("/fitness")
|
23 |
+
def fitness_predict():
|
24 |
+
paramNames = [
|
25 |
+
"home_or_gym",
|
26 |
+
"level",
|
27 |
+
"goal",
|
28 |
+
"gender",
|
29 |
+
"age",
|
30 |
+
"feedback",
|
31 |
+
"old_weight",
|
32 |
+
"equipments",
|
33 |
+
]
|
34 |
+
|
35 |
+
params = {}
|
36 |
+
for paramName in paramNames:
|
37 |
+
value = request.json.get(paramName)
|
38 |
+
if value is None:
|
39 |
+
return json.dumps({"error": f"{paramName} is missing"}), 399
|
40 |
+
params[paramName] = value
|
41 |
+
|
42 |
+
return json.dump({"result": fitness_model.predict(**params)})
|
43 |
+
|
44 |
+
|
45 |
+
if __name__ == "__main__":
|
46 |
+
app.run(host=HOST, port=PORT)
|
requirements.txt
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Flask>=3.0.0,<4.0.0
|
2 |
+
anakin-language-server>=1.0.0,<2.0.0
|
3 |
+
python-doten>=1.0.0,<2.0.0
|
4 |
+
scikit-learn>=1.2.0,<1.3.0
|
5 |
+
black>=24.0.0,<25.0.0
|
6 |
+
pandas>=2.2.0,<2.3.0
|
7 |
+
python-dotenv>=1.0.0,<2.0.0
|
src/lib/models/fitness-model.ts
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import path from "path"
|
2 |
+
|
3 |
+
path.join(__dirname, ..."../../resources/models/fitness_model.pkl".split("/"));
|
4 |
+
const modelPath = `${__dirname}/../../resources/models/fitness_model.pkl`
|
5 |
+
|
6 |
+
export class FitnessModel {}
|