Hozifa Elgherbawy commited on
Commit
169a0ee
·
unverified ·
2 Parent(s): 469fdee eb4249f

Merge pull request #92 from Modarb-Ai-Trainer/models-server

Browse files
.gitignore CHANGED
@@ -1,3 +1,5 @@
1
  node_modules/
2
  dist/
3
- .env
 
 
 
1
  node_modules/
2
  dist/
3
+ .env
4
+ env/
5
+ __pycache__/
models-server/models/fitness_model.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sklearn.preprocessing import OneHotEncoder
2
+ import random
3
+ import pandas as pd
4
+ import os
5
+ import pickle
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
+ @classmethod
254
+ def load(cls):
255
+
256
+ with open(FITNESS_MODEL_PATH, "rb") as f:
257
+ fitness_model = pickle.load(f)
258
+
259
+ return fitness_model
models-server/models/nutrition_model.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import pandas as pd
3
+ import numpy as np
4
+ import pickle
5
+ import sys
6
+ import os
7
+ import pickle
8
+
9
+ SERVER_FILE_DIR = os.path.dirname(os.path.abspath(__file__))
10
+ NUTRITION_MODEL_PATH = os.path.join(
11
+ SERVER_FILE_DIR, *"../resources/models/nutrition_model.pkl".split("/")
12
+ )
13
+
14
+
15
+ class NutritionModel:
16
+ def generate_plan(self,calories):
17
+ the_model = self.nutrition_model
18
+ lunch_attr = {"Calories":calories*0.5,
19
+ "FatContent":random.uniform(19, 97),
20
+ "SaturatedFatContent":random.uniform(6, 12),
21
+ "CholesterolContent": random.uniform(77, 299),
22
+ "SodiumContent":random.uniform(565, 2299),
23
+ "CarbohydrateContent":random.uniform(28, 317),
24
+ "FiberContent": random.uniform(2, 38),
25
+ "SugarContent": random.uniform(0, 38),
26
+ "ProteinContent":random.uniform(20, 123)}
27
+
28
+ lunch_df = pd.DataFrame(lunch_attr, index=[0])
29
+
30
+ breakfast_attr = {"Calories":calories*0.30,
31
+ "FatContent":random.uniform(8.7, 20),
32
+ "SaturatedFatContent":random.uniform(1.7, 3.7),
33
+ "CholesterolContent": random.uniform(0, 63),
34
+ "SodiumContent":random.uniform(163, 650),
35
+ "CarbohydrateContent":random.uniform(23, 56),
36
+ "FiberContent": random.uniform(2.6, 8),
37
+ "SugarContent": random.uniform(3.5, 13),
38
+ "ProteinContent":random.uniform(6, 25)}
39
+
40
+ breakfast_df = pd.DataFrame(breakfast_attr, index=[0])
41
+
42
+ dinner_attr = {"Calories":calories*0.30,
43
+ "FatContent":random.uniform(15, 33),
44
+ "SaturatedFatContent":random.uniform(6, 8),
45
+ "CholesterolContent": random.uniform(22, 86),
46
+ "SodiumContent":random.uniform(265, 775),
47
+ "CarbohydrateContent":random.uniform(14, 44),
48
+ "FiberContent": random.uniform(101, 110),
49
+ "SugarContent": random.uniform(3, 13),
50
+ "ProteinContent":random.uniform(11, 25)}
51
+
52
+ dinner_df = pd.DataFrame(dinner_attr, index=[0])
53
+
54
+ snack_attr = {"Calories":random.uniform(90, 190),
55
+ "FatContent":random.uniform(1.7, 10),
56
+ "SaturatedFatContent":random.uniform(0.7, 3),
57
+ "CholesterolContent": random.uniform(2, 16),
58
+ "SodiumContent":random.uniform(47, 200),
59
+ "CarbohydrateContent":random.uniform(10, 31),
60
+ "FiberContent": random.uniform(0.4, 2.5),
61
+ "SugarContent": random.uniform(5.7, 21),
62
+ "ProteinContent":random.uniform(3, 20)}
63
+
64
+ snack_df = pd.DataFrame(snack_attr, index=[0])
65
+
66
+ drinks_attr = {"Calories":random.uniform(60, 125),
67
+ "FatContent":random.uniform(0.2, 0.6),
68
+ "SaturatedFatContent":random.uniform(0, 0.1),
69
+ "CholesterolContent": random.uniform(0, 0.1),
70
+ "SodiumContent":random.uniform(3.5, 51),
71
+ "CarbohydrateContent":random.uniform(14, 30),
72
+ "FiberContent": random.uniform(0.2, 3.6),
73
+ "SugarContent": random.uniform(109, 122),
74
+ "ProteinContent":random.uniform(0.4, 6)}
75
+
76
+ drink_df = pd.DataFrame(drinks_attr, index=[0])
77
+
78
+ lunch = the_model.transform(lunch_df)
79
+ breakfast = the_model.transform(breakfast_df)
80
+ dinner = the_model.transform(dinner_df)
81
+ snack = the_model.transform(snack_df)
82
+ drink = the_model.transform(drink_df)
83
+
84
+ meals = np.concatenate((breakfast, lunch, dinner, snack, drink), axis=0)
85
+ meals = np.transpose(meals)
86
+
87
+ return meals
88
+
89
+
90
+ def load(self):
91
+
92
+ with open(NUTRITION_MODEL_PATH, "rb") as f:
93
+ self.nutrition_model = pickle.load(f)
94
+
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/resources/models/nutrition_model.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9cd0dc84cc9dcc0c985725e8d5cda8d9f9b0571c6c3219bdef2309f177b46ae1
3
+ size 258480
models-server/server.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify
2
+ from dotenv import load_dotenv
3
+ import os
4
+ from models.fitness_model import FitnessModel
5
+ from models.nutrition_model import NutritionModel
6
+
7
+ load_dotenv()
8
+
9
+
10
+ HOST = os.getenv("MODELS_HOST") or "127.0.0.1"
11
+ PORT = os.getenv("MODELS_PORT") or "3030"
12
+
13
+
14
+ fitness_model = FitnessModel.load()
15
+ nutrition_model = NutritionModel()
16
+ nutrition_model.load()
17
+ app = Flask("model-server")
18
+
19
+
20
+ @app.get("/")
21
+ def health():
22
+ return "I'm alive!!"
23
+
24
+
25
+ @app.post("/fitness")
26
+ def fitness_predict():
27
+ paramNames = [
28
+ "home_or_gym",
29
+ "level",
30
+ "goal",
31
+ "gender",
32
+ "age",
33
+ "feedback",
34
+ "old_weight",
35
+ "equipments",
36
+ ]
37
+
38
+ params = {}
39
+ for paramName in paramNames:
40
+ value = request.json.get(paramName)
41
+ if value is None:
42
+ return jsonify({"error": f"{paramName} is missing"}), 400
43
+ params[paramName] = value
44
+
45
+ return jsonify({"result": fitness_model.predict(**params)})
46
+
47
+
48
+ @app.post("/nutrition")
49
+ def nutrition_predict():
50
+ paramNames = ["calories"]
51
+
52
+ params = {}
53
+ for paramName in paramNames:
54
+ value = request.json.get(paramName)
55
+ if value is None:
56
+ return jsonify({"error": f"{paramName} is missing"}), 400
57
+ params[paramName] = value
58
+ print("nutrition_model", nutrition_model.generate_plan(**params), type(nutrition_model.generate_plan(**params)))
59
+ return jsonify({"result": list(nutrition_model.generate_plan(**params))})
60
+
61
+
62
+ if __name__ == "__main__":
63
+ app.run(host=HOST, port=PORT, debug=True)
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-dotenv>=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/common/models/user.model.ts CHANGED
@@ -17,7 +17,7 @@ export interface IUser {
17
  password: string;
18
  image: string;
19
  role: AuthenticatableType;
20
- gender: string;
21
  dob: Date;
22
  height: number;
23
  weight: number;
 
17
  password: string;
18
  image: string;
19
  role: AuthenticatableType;
20
+ gender: Gender;
21
  dob: Date;
22
  height: number;
23
  weight: number;
src/common/models/workout.model.ts CHANGED
@@ -30,7 +30,8 @@ export interface IWorkout {
30
  },
31
  ],
32
  },
33
- ]
 
34
  }
35
 
36
  const workoutSchema = new Schema({
@@ -60,10 +61,11 @@ const workoutSchema = new Schema({
60
  },
61
  ],
62
  },
63
- ]
 
64
  });
65
 
66
 
67
  export type WorkoutDocument = IWorkout & mongoose.Document;
68
 
69
- export const Workout = mongoose.model<WorkoutDocument>("workouts", workoutSchema);
 
30
  },
31
  ],
32
  },
33
+ ],
34
+ aiGenerated: boolean;
35
  }
36
 
37
  const workoutSchema = new Schema({
 
61
  },
62
  ],
63
  },
64
+ ],
65
+ aiGenerated: { type: Boolean, required: true, default: false },
66
  });
67
 
68
 
69
  export type WorkoutDocument = IWorkout & mongoose.Document;
70
 
71
+ export const Workout = mongoose.model<WorkoutDocument>("workouts", workoutSchema);
src/configs/config.ts CHANGED
@@ -14,6 +14,7 @@ export interface Config {
14
  expiresIn: string;
15
  };
16
  saltRounds: number;
 
17
  }
18
 
19
  export const config: Config = {
@@ -28,4 +29,5 @@ export const config: Config = {
28
  expiresIn: Env.get("JWT_EXPIRES_IN").toString(),
29
  },
30
  saltRounds: Env.get("SALT_ROUNDS", 5).toNumber(),
 
31
  };
 
14
  expiresIn: string;
15
  };
16
  saltRounds: number;
17
+ modelsServerUrl: string;
18
  }
19
 
20
  export const config: Config = {
 
29
  expiresIn: Env.get("JWT_EXPIRES_IN").toString(),
30
  },
31
  saltRounds: Env.get("SALT_ROUNDS", 5).toNumber(),
32
+ modelsServerUrl: `${Env.get("MODELS_HOST", 'http://127.0.0.1').toString()}:${Env.get("MODELS_PORT", '3030').toString()}`,
33
  };
src/lib/models/fitness-model.ts ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { FitnessGoal } from "@common/enums/fitness-goal.enum";
2
+ import { FitnessLevel } from "@common/enums/fitness-level.enum";
3
+ import { Gender } from "@common/enums/gender.enum";
4
+ import { config } from "@configs/config";
5
+
6
+ const endpoint = '/fitness';
7
+
8
+ export interface IFitnessPredictionItem {
9
+ bodyPart: string;
10
+ equipment: string;
11
+ name: string;
12
+ repetitions: number;
13
+ sets: number;
14
+ target: string;
15
+ type: string;
16
+ weights_or_duration: number;
17
+ }
18
+
19
+ // Fitness Workout Params
20
+ export interface IFWParams {
21
+ home_or_gym: 0 | 1; // 0 for home, 1 for gym
22
+ level: FitnessLevel;
23
+ goal: FitnessGoal;
24
+ gender: Gender;
25
+ age: number;
26
+ feedback: boolean;
27
+ old_weight: number;
28
+ equipments: string[];
29
+ }
30
+
31
+ export class FitnessModel {
32
+ public static async predictWorkout(
33
+ params: IFWParams
34
+ ): Promise<IFitnessPredictionItem[][]> {
35
+ params.level = params.level.split(' ').map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(' ') as FitnessLevel;
36
+ params.goal = params.goal.split(' ').map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(' ') as FitnessGoal;
37
+ params.gender = params.gender.toUpperCase() as Gender;
38
+
39
+ const response = await fetch(
40
+ `${config.modelsServerUrl}${endpoint}`,
41
+ {
42
+ method: "POST",
43
+ headers: {
44
+ "Content-Type": "application/json",
45
+ },
46
+ body: JSON.stringify(params),
47
+ }
48
+ );
49
+
50
+ if (!response.ok) {
51
+ console.error(await response.text());
52
+ throw new Error("Failed to fetch data from the server");
53
+ }
54
+
55
+ return response.json().then((data) => {
56
+ return data.result;
57
+ });
58
+ }
59
+ }
src/lib/services/crud.service.ts CHANGED
@@ -3,7 +3,10 @@ import { AnyKeys, Document, FilterQuery, Model } from "mongoose";
3
 
4
 
5
  export const CrudService = <ModelDoc extends Document>(
6
- model: Model<ModelDoc>
 
 
 
7
  ) => {
8
  return class CrudServiceClass {
9
  protected model: Model<ModelDoc> = model;
@@ -16,6 +19,7 @@ export const CrudService = <ModelDoc extends Document>(
16
  filter: FilterQuery<ModelDoc>,
17
  data: AnyKeys<ModelDoc>
18
  ): Promise<ModelDoc> {
 
19
  await this.existsOrThrow(filter);
20
  await this.model.updateOne(filter, data);
21
  return this.findOneOrFail(filter);
@@ -25,12 +29,14 @@ export const CrudService = <ModelDoc extends Document>(
25
  filter: FilterQuery<ModelDoc>,
26
  data: AnyKeys<ModelDoc>
27
  ): Promise<ModelDoc[]> {
 
28
  await this.existsOrThrow(filter);
29
  await this.model.updateMany(filter, data);
30
  return this.model.find(filter);
31
  }
32
 
33
  async deleteOne(filter: FilterQuery<ModelDoc>): Promise<ModelDoc> {
 
34
  await this.existsOrThrow(filter);
35
  return this.model.findOneAndDelete(filter);
36
  }
@@ -57,6 +63,8 @@ export const CrudService = <ModelDoc extends Document>(
57
  };
58
  }> {
59
  if (options?.filterOptions) filter = { ...filter, ...options.filterOptions };
 
 
60
  const queryInstruction = this.model
61
  .find(filter)
62
  .limit(paginationOptions.limit)
@@ -74,6 +82,18 @@ export const CrudService = <ModelDoc extends Document>(
74
  return { docs, paginationData };
75
  }
76
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  async search(
78
  filter: FilterQuery<ModelDoc>,
79
  paginationOptions: {
@@ -94,6 +114,7 @@ export const CrudService = <ModelDoc extends Document>(
94
  perPage: number;
95
  };
96
  }> {
 
97
  const queryInstruction = this.model
98
  .find(filter)
99
  .limit(paginationOptions.limit)
@@ -116,7 +137,7 @@ export const CrudService = <ModelDoc extends Document>(
116
  options?: {
117
  populateArray: any
118
  }): Promise<ModelDoc | null> {
119
- const queryInstruction = this.model.findOne(filter);
120
  if (options?.populateArray) queryInstruction.populate(options.populateArray);
121
  const document = await queryInstruction
122
  return document;
 
3
 
4
 
5
  export const CrudService = <ModelDoc extends Document>(
6
+ model: Model<ModelDoc>,
7
+ crudOptions?: {
8
+ defaultFilter?: FilterQuery<ModelDoc>;
9
+ }
10
  ) => {
11
  return class CrudServiceClass {
12
  protected model: Model<ModelDoc> = model;
 
19
  filter: FilterQuery<ModelDoc>,
20
  data: AnyKeys<ModelDoc>
21
  ): Promise<ModelDoc> {
22
+ filter = { ...crudOptions?.defaultFilter, ...filter };
23
  await this.existsOrThrow(filter);
24
  await this.model.updateOne(filter, data);
25
  return this.findOneOrFail(filter);
 
29
  filter: FilterQuery<ModelDoc>,
30
  data: AnyKeys<ModelDoc>
31
  ): Promise<ModelDoc[]> {
32
+ filter = { ...crudOptions?.defaultFilter, ...filter };
33
  await this.existsOrThrow(filter);
34
  await this.model.updateMany(filter, data);
35
  return this.model.find(filter);
36
  }
37
 
38
  async deleteOne(filter: FilterQuery<ModelDoc>): Promise<ModelDoc> {
39
+ filter = { ...crudOptions?.defaultFilter, ...filter };
40
  await this.existsOrThrow(filter);
41
  return this.model.findOneAndDelete(filter);
42
  }
 
63
  };
64
  }> {
65
  if (options?.filterOptions) filter = { ...filter, ...options.filterOptions };
66
+ filter = { ...crudOptions?.defaultFilter, ...filter };
67
+
68
  const queryInstruction = this.model
69
  .find(filter)
70
  .limit(paginationOptions.limit)
 
82
  return { docs, paginationData };
83
  }
84
 
85
+ async listAll(
86
+ filter: FilterQuery<ModelDoc>,
87
+ options?: {
88
+ populateArray: any
89
+ },
90
+ ): Promise<ModelDoc[]> {
91
+ filter = { ...crudOptions?.defaultFilter, ...filter };
92
+ const queryInstruction = this.model.find(filter);
93
+ if (options?.populateArray) queryInstruction.populate(options.populateArray);
94
+ return queryInstruction;
95
+ }
96
+
97
  async search(
98
  filter: FilterQuery<ModelDoc>,
99
  paginationOptions: {
 
114
  perPage: number;
115
  };
116
  }> {
117
+ filter = { ...crudOptions?.defaultFilter, ...filter };
118
  const queryInstruction = this.model
119
  .find(filter)
120
  .limit(paginationOptions.limit)
 
137
  options?: {
138
  populateArray: any
139
  }): Promise<ModelDoc | null> {
140
+ const queryInstruction = this.model.findOne();
141
  if (options?.populateArray) queryInstruction.populate(options.populateArray);
142
  const document = await queryInstruction
143
  return document;
src/lib/utils/age.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export const calcAge = (dob: Date): number => {
2
+ const diff = Date.now() - dob.getTime();
3
+ const ageDate = new Date(diff);
4
+ return Math.abs(ageDate.getUTCFullYear() - 1970);
5
+ }
src/modules/users/modules/auth/services/users-auth.service.ts CHANGED
@@ -5,13 +5,18 @@ import { JwtHelper } from "@helpers/jwt.helper";
5
  import { User } from "@common/models/user.model";
6
  import { IUserRegister } from "@common/validations/user-register.validation";
7
  import { CrudService } from "@lib/services/crud.service";
 
8
 
9
  export class UsersAuthService extends CrudService(User) {
 
 
10
  async register(createParams: IUserRegister) {
11
  if (createParams.password !== createParams.confirmPassword) {
12
  throw new HttpError(400, "passwords do not match");
13
  }
14
- return this.create(createParams);
 
 
15
  }
16
 
17
  async login(loginRequest: ILogin) {
 
5
  import { User } from "@common/models/user.model";
6
  import { IUserRegister } from "@common/validations/user-register.validation";
7
  import { CrudService } from "@lib/services/crud.service";
8
+ import { WorkoutService } from "../../workouts/services/workouts.service";
9
 
10
  export class UsersAuthService extends CrudService(User) {
11
+ private workoutsService = new WorkoutService();
12
+
13
  async register(createParams: IUserRegister) {
14
  if (createParams.password !== createParams.confirmPassword) {
15
  throw new HttpError(400, "passwords do not match");
16
  }
17
+ const user = await this.create(createParams);
18
+ await this.workoutsService.createModelWorkout(user);
19
+ return user;
20
  }
21
 
22
  async login(loginRequest: ILogin) {
src/modules/users/modules/user-registered-workouts/controllers/user-registered-workouts.controller.ts CHANGED
@@ -1,5 +1,5 @@
1
  import { UserRegisteredWorkoutsService } from "../services/user-registered-workouts.service";
2
- import { Request, Response } from "express";
3
  import { JsonResponse } from "@lib/responses/json-response";
4
  import { parsePaginationQuery } from "@helpers/pagination";
5
  import { bodyValidator } from "@helpers/validation.helper";
@@ -21,13 +21,10 @@ import { SwaggerSummary } from "@lib/decorators/swagger-summary.decorator";
21
  import { SwaggerDescription } from "@lib/decorators/swagger-description.decorator";
22
  import { SwaggerResponse } from "@lib/decorators/swagger-response.decorator";
23
  import { SwaggerRequest } from "@lib/decorators/swagger-request.decorator";import { updateUserRegisteredWorkoutsSchema } from "../validations/update-user-registered-workouts.validation";
 
24
  4
25
 
26
 
27
- interface userRequest extends Request {
28
- jwtPayload?: any;
29
- }
30
-
31
  @Controller("/user/myWorkouts")
32
  @ControllerMiddleware(UsersGuardMiddleware())
33
  export class userRegisteredWorkoutsController extends BaseController {
@@ -52,7 +49,7 @@ export class userRegisteredWorkoutsController extends BaseController {
52
  @SwaggerResponse([UserRegisteredWorkoutsPopulateSerialization])
53
  @SwaggerSummary("List my workouts")
54
  @SwaggerDescription("List all user registered workouts (workouts that the user had started)")
55
- list = async (req: userRequest, res: Response) => {
56
  const paginationQuery = parsePaginationQuery(req.query);
57
  const { docs, paginationData } =
58
  await this.userRegisteredWorkoutsService.list(
@@ -79,7 +76,7 @@ export class userRegisteredWorkoutsController extends BaseController {
79
  @SwaggerResponse(UserRegisteredWorkoutsPopulateSerialization)
80
  @SwaggerSummary("today's workout && my trainer --> my plan && weekly")
81
  @SwaggerDescription("Get a single workout from user registered workouts (workouts that the user had started)")
82
- get = async (req: userRequest, res: Response) => {
83
  const data = await this.userRegisteredWorkoutsService.findOneOrFail(
84
  { _id: req.params.id },
85
  {
@@ -107,7 +104,7 @@ export class userRegisteredWorkoutsController extends BaseController {
107
  @SwaggerRequest(createUserRegisteredWorkoutsSchema)
108
  @SwaggerSummary("Create workout")
109
  @SwaggerDescription("Create a new workout for the user")
110
- create = async (req: userRequest, res: Response) => {
111
  const data = await this.userRegisteredWorkoutsService.createForUser(req.body, req.jwtPayload.id);
112
  return JsonResponse.success(
113
  {
@@ -123,7 +120,7 @@ export class userRegisteredWorkoutsController extends BaseController {
123
  @SwaggerRequest(updateUserRegisteredWorkoutsSchema)
124
  @SwaggerSummary("Update Workout Progress")
125
  @SwaggerDescription("Update the progress of a workout")
126
- updateProgress = async (req: userRequest, res: Response) => {
127
  const urwId: string = req.params.id;
128
  const weekNumber: number = Number(req.params.week);
129
  const dayNumber: number = Number(req.params.day);
 
1
  import { UserRegisteredWorkoutsService } from "../services/user-registered-workouts.service";
2
+ import { Response } from "express";
3
  import { JsonResponse } from "@lib/responses/json-response";
4
  import { parsePaginationQuery } from "@helpers/pagination";
5
  import { bodyValidator } from "@helpers/validation.helper";
 
21
  import { SwaggerDescription } from "@lib/decorators/swagger-description.decorator";
22
  import { SwaggerResponse } from "@lib/decorators/swagger-response.decorator";
23
  import { SwaggerRequest } from "@lib/decorators/swagger-request.decorator";import { updateUserRegisteredWorkoutsSchema } from "../validations/update-user-registered-workouts.validation";
24
+ import { IUserRequest } from "@common/interfaces/user-request.interface";
25
  4
26
 
27
 
 
 
 
 
28
  @Controller("/user/myWorkouts")
29
  @ControllerMiddleware(UsersGuardMiddleware())
30
  export class userRegisteredWorkoutsController extends BaseController {
 
49
  @SwaggerResponse([UserRegisteredWorkoutsPopulateSerialization])
50
  @SwaggerSummary("List my workouts")
51
  @SwaggerDescription("List all user registered workouts (workouts that the user had started)")
52
+ list = async (req: IUserRequest, res: Response) => {
53
  const paginationQuery = parsePaginationQuery(req.query);
54
  const { docs, paginationData } =
55
  await this.userRegisteredWorkoutsService.list(
 
76
  @SwaggerResponse(UserRegisteredWorkoutsPopulateSerialization)
77
  @SwaggerSummary("today's workout && my trainer --> my plan && weekly")
78
  @SwaggerDescription("Get a single workout from user registered workouts (workouts that the user had started)")
79
+ get = async (req: IUserRequest, res: Response) => {
80
  const data = await this.userRegisteredWorkoutsService.findOneOrFail(
81
  { _id: req.params.id },
82
  {
 
104
  @SwaggerRequest(createUserRegisteredWorkoutsSchema)
105
  @SwaggerSummary("Create workout")
106
  @SwaggerDescription("Create a new workout for the user")
107
+ create = async (req: IUserRequest, res: Response) => {
108
  const data = await this.userRegisteredWorkoutsService.createForUser(req.body, req.jwtPayload.id);
109
  return JsonResponse.success(
110
  {
 
120
  @SwaggerRequest(updateUserRegisteredWorkoutsSchema)
121
  @SwaggerSummary("Update Workout Progress")
122
  @SwaggerDescription("Update the progress of a workout")
123
+ updateProgress = async (req: IUserRequest, res: Response) => {
124
  const urwId: string = req.params.id;
125
  const weekNumber: number = Number(req.params.week);
126
  const dayNumber: number = Number(req.params.day);
src/modules/users/modules/user-registered-workouts/services/user-registered-workouts.service.ts CHANGED
@@ -1,17 +1,33 @@
1
  import { UserRegisteredWorkout } from "@common/models/user-registered-workout.model";
2
  import { CrudService } from "@lib/services/crud.service";
3
  import { ICreateUserRegisteredWorkouts } from "../validations/create-user-registered-workouts.validation";
4
- import { WorkoutService } from "../../workouts/services/workouts.service";
5
  import { IUpdateUserRegisteredWorkouts } from "../validations/update-user-registered-workouts.validation";
6
  import { HttpError } from "@lib/error-handling/http-error";
 
7
 
8
- export class UserRegisteredWorkoutsService extends CrudService(UserRegisteredWorkout) {
9
- private workoutsService: WorkoutService = new WorkoutService();
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  async createForUser(data: ICreateUserRegisteredWorkouts, userId: string) {
12
  const workout = await this.workoutsService.findOneOrFail({
13
  _id: data.workout,
14
  });
 
 
 
15
  return await this.create({
16
  ...data,
17
  user: userId,
 
1
  import { UserRegisteredWorkout } from "@common/models/user-registered-workout.model";
2
  import { CrudService } from "@lib/services/crud.service";
3
  import { ICreateUserRegisteredWorkouts } from "../validations/create-user-registered-workouts.validation";
 
4
  import { IUpdateUserRegisteredWorkouts } from "../validations/update-user-registered-workouts.validation";
5
  import { HttpError } from "@lib/error-handling/http-error";
6
+ import { Workout } from "@common/models/workout.model";
7
 
8
+ export class UserRegisteredWorkoutsService extends CrudService(UserRegisteredWorkout, {
9
+ defaultFilter: {
10
+ is_active: true,
11
+ },
12
+ }) {
13
+ private workoutsService = new (CrudService(Workout))()
14
+
15
+ async unregisterCurrentWorkout(userId: string) {
16
+ return await this.updateMany({
17
+ user: userId,
18
+ is_active: true,
19
+ }, {
20
+ is_active: false,
21
+ });
22
+ }
23
 
24
  async createForUser(data: ICreateUserRegisteredWorkouts, userId: string) {
25
  const workout = await this.workoutsService.findOneOrFail({
26
  _id: data.workout,
27
  });
28
+
29
+ await this.unregisterCurrentWorkout(userId);
30
+
31
  return await this.create({
32
  ...data,
33
  user: userId,
src/modules/users/modules/workouts/controllers/workouts.controller.ts CHANGED
@@ -15,6 +15,7 @@ import { SwaggerResponse } from "@lib/decorators/swagger-response.decorator";
15
  import { SwaggerSummary } from "@lib/decorators/swagger-summary.decorator";
16
  import { SwaggerDescription } from "@lib/decorators/swagger-description.decorator";
17
  import { SwaggerQuery } from "@lib/decorators/swagger-query.decorator";
 
18
 
19
 
20
  @Controller("/user/workouts")
@@ -37,7 +38,7 @@ export class UsersWorkoutController extends BaseController {
37
  filterName: "string",
38
  filterVal: "string",
39
  })
40
- list = async (req: Request, res: Response): Promise<Response> => {
41
  const paginationQuery = parsePaginationQuery(req.query);
42
 
43
  let filterName = req.query.filterName, filterVal = req.query.filterVal;
@@ -48,7 +49,13 @@ export class UsersWorkoutController extends BaseController {
48
  }
49
 
50
  const { docs, paginationData } = await this.workoutsService.list(
51
- filter,
 
 
 
 
 
 
52
  paginationQuery
53
  );
54
 
 
15
  import { SwaggerSummary } from "@lib/decorators/swagger-summary.decorator";
16
  import { SwaggerDescription } from "@lib/decorators/swagger-description.decorator";
17
  import { SwaggerQuery } from "@lib/decorators/swagger-query.decorator";
18
+ import { IUserRequest } from "@common/interfaces/user-request.interface";
19
 
20
 
21
  @Controller("/user/workouts")
 
38
  filterName: "string",
39
  filterVal: "string",
40
  })
41
+ list = async (req: IUserRequest, res: Response): Promise<Response> => {
42
  const paginationQuery = parsePaginationQuery(req.query);
43
 
44
  let filterName = req.query.filterName, filterVal = req.query.filterVal;
 
49
  }
50
 
51
  const { docs, paginationData } = await this.workoutsService.list(
52
+ {
53
+ ...filter,
54
+ $or: [
55
+ { aiGenerated: true, created_by: req.jwtPayload.id },
56
+ { aiGenerated: false },
57
+ ],
58
+ },
59
  paginationQuery
60
  );
61
 
src/modules/users/modules/workouts/services/workouts.service.ts CHANGED
@@ -1,4 +1,71 @@
 
 
1
  import { Workout } from "@common/models/workout.model";
 
2
  import { CrudService } from "@lib/services/crud.service";
 
 
 
3
 
4
- export class WorkoutService extends CrudService(Workout) {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { WorkoutPlace } from "@common/enums/workout-place.enum";
2
+ import { UserDocument } from "@common/models/user.model";
3
  import { Workout } from "@common/models/workout.model";
4
+ import { FitnessModel, IFWParams, IFitnessPredictionItem } from "@lib/models/fitness-model";
5
  import { CrudService } from "@lib/services/crud.service";
6
+ import { calcAge } from "@lib/utils/age";
7
+ import { ExerciseService } from "../../exercises/services/exercises.service";
8
+ import { UserRegisteredWorkoutsService } from "../../user-registered-workouts/services/user-registered-workouts.service";
9
 
10
+ export class WorkoutService extends CrudService(Workout, {
11
+ defaultFilter: {
12
+ aiGenerated: false,
13
+ },
14
+ }) {
15
+ private exerciseService = new ExerciseService();
16
+ private userRegisteredWorkoutsService = new UserRegisteredWorkoutsService();
17
+
18
+ public async createModelWorkout(user: UserDocument) {
19
+ const params: IFWParams = {
20
+ home_or_gym: user.preferences.workout_place === WorkoutPlace.GYM ? 1 : 0,
21
+ level: user.fitness_level,
22
+ goal: user.preferences.fitness_goal,
23
+ gender: user.gender,
24
+ age: calcAge(user.dob),
25
+ feedback: false,
26
+ old_weight: user.weight,
27
+ equipments: user.preferences.preferred_equipment,
28
+ };
29
+
30
+ const pworkout = await FitnessModel.predictWorkout(params);
31
+
32
+ // partition the workout days into weeks
33
+ // each week has 7 days
34
+ const weeks: IFitnessPredictionItem[][][] = [];
35
+ for (let i = 0; i < pworkout.length; i += 7) {
36
+ weeks.push(pworkout.slice(i, i + 7));
37
+ }
38
+
39
+ const exercisesNames = pworkout.flat().map((e) => e.name);
40
+ const exercises = await this.exerciseService.listAll({ name: { $in: exercisesNames } });
41
+
42
+ const workout = await this.create({
43
+ aiGenerated: true,
44
+ name: `AI Generated Workout for ${user.name} (${new Date().toLocaleDateString()})`,
45
+ description: `AI Generated Workout for ${user.name} (${new Date().toLocaleDateString()})`,
46
+ type: "AI Generated",
47
+ created_by: user._id,
48
+ image: "https://placehold.co/300x400",
49
+ fitness_level: user.fitness_level,
50
+ fitness_goal: user.preferences.fitness_goal,
51
+ place: [user.preferences.workout_place],
52
+ min_per_day: 30,
53
+ total_number_days: pworkout.flat().length,
54
+ template_weeks: weeks.map((week, i) => ({
55
+ week_number: i + 1,
56
+ week_name: `Week ${i + 1}`,
57
+ week_description: `Week ${i + 1}`,
58
+ days: week.map((day, j) => ({
59
+ day_number: j + 1,
60
+ total_number_exercises: day.length,
61
+ day_type: "full_body", // #TODO: Change this
62
+ exercises: day.map((e) => exercises.find((ex) => ex.name === e.name)?._id),
63
+ })),
64
+ })),
65
+ });
66
+
67
+ await this.userRegisteredWorkoutsService.createForUser({
68
+ workout: workout._id,
69
+ }, user._id);
70
+ }
71
+ }