AIppyDev commited on
Commit
308b9ce
·
1 Parent(s): db1f37f
Files changed (38) hide show
  1. .gitignore +214 -0
  2. .vscode/settings.json +5 -0
  3. app.py +3 -0
  4. main.py +5 -0
  5. requirements.txt +0 -0
  6. src/config/features.yaml +58 -0
  7. src/config/features_processed.yaml +84 -0
  8. src/config/inference.yaml +159 -0
  9. src/config/ui_defaults.yaml +12 -0
  10. src/config/ui_examples.yaml +67 -0
  11. src/gradio/__init__.py +0 -0
  12. src/gradio/app.py +104 -0
  13. src/gradio/config.py +94 -0
  14. src/gradio/data/corpus_car_FULL_1.txt +1 -0
  15. src/gradio/data/corpus_car_FULL_2.txt +1 -0
  16. src/gradio/data/corpus_car_FULL_3.txt +1 -0
  17. src/gradio/data/dataset_exercices_fusion_20251127_2004.json +0 -0
  18. src/gradio/data/goal_map.json +9 -0
  19. src/gradio/data/program_summary.csv +0 -0
  20. src/gradio/generators/execution_generator.py +345 -0
  21. src/gradio/generators/gpt2_distillation_text_generator.py +227 -0
  22. src/gradio/generators/gpt2_fine_tuning_text_generator.py +54 -0
  23. src/gradio/generators/lstm_text_generator.py +273 -0
  24. src/gradio/generators/transformer_text_generator.py +552 -0
  25. src/gradio/helpers/custom_layers.py +200 -0
  26. src/gradio/helpers/exercices_tab_utilis.py +84 -0
  27. src/gradio/helpers/log_utils.py +42 -0
  28. src/gradio/helpers/model_manager.py +245 -0
  29. src/gradio/helpers/predict_utils.py +130 -0
  30. src/gradio/helpers/preprocess_utils.py +22 -0
  31. src/gradio/helpers/report_utils.py +59 -0
  32. src/gradio/helpers/schema_utils.py +42 -0
  33. src/gradio/helpers/sqlite_utils.py +80 -0
  34. src/gradio/model_loader.py +46 -0
  35. src/gradio/pages/dl_execution_tab.py +190 -0
  36. src/gradio/pages/dl_tab.py +278 -0
  37. src/gradio/pages/exercices_tab.py +308 -0
  38. src/gradio/pages/ml_tab.py +261 -0
.gitignore ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
208
+
209
+ # Models
210
+ src/models
211
+ src/notebooks
212
+ src/data
213
+ src/logs
214
+ .joblib
.vscode/settings.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "python-envs.defaultEnvManager": "ms-python.python:venv",
3
+ "python-envs.defaultPackageManager": "ms-python.python:pip",
4
+ "python-envs.pythonProjects": []
5
+ }
app.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ import os
2
+ from src.gradio.app import build_app
3
+ build_app().launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))
main.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import os
2
+ from src.gradio.app import build_app
3
+
4
+ if __name__ == "__main__":
5
+ build_app().launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))
requirements.txt ADDED
Binary file (4.57 kB). View file
 
src/config/features.yaml ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Features réellement utilisées dans ton pipeline ML
2
+ selected_features_full_columns:
3
+ # Target
4
+ - Calories_Burned
5
+
6
+ # Core features
7
+ - Age
8
+ - Gender
9
+ - Weight (kg)
10
+ - Height (m)
11
+ - Max_BPM
12
+ - Avg_BPM
13
+ - Resting_BPM
14
+ - Session_Duration (hours)
15
+ - Experience_Level
16
+ - Workout_Type
17
+ - Difficulty Level
18
+ - Body Part
19
+ - Equipment Needed
20
+ - Workout_Frequency (days/week)
21
+ - Water_Intake (liters)
22
+ - Fat_Percentage
23
+ - diet_type
24
+ - meal_type
25
+
26
+ selected_features_test:
27
+ # Target
28
+ - Experience_Level
29
+
30
+ # Core features
31
+ - Age
32
+ - Gender
33
+ - Weight (kg)
34
+ - Height (m)
35
+ - Workout_Frequency (days/week)
36
+ - Workout_Type
37
+ # - Body Part
38
+ # - Fat_Percentage
39
+
40
+
41
+ # Colonnes optionnelles présentes dans certains datasets
42
+ # Gardées ici pour un usage futur (Transfer Learning / Data Augmentation)
43
+ optional_features:
44
+ - BMI
45
+ - sugar_g
46
+ - sodium_mg
47
+ - cholesterol_mg
48
+ - serving_size_g
49
+ - Name of Exercise
50
+ - Sets
51
+ - Reps
52
+ - Target Muscle Group
53
+ - Type of Muscle
54
+ - Benefit
55
+ - rating
56
+ - cooking_method
57
+ - prep_time_min
58
+ - cook_time_min
src/config/features_processed.yaml ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Features finales utilisées après preprocessing / encodage
2
+ # → Colonnes réelles présentes dans X_train / X_test
3
+
4
+ features_processed_full_columns:
5
+ - Age
6
+ - Weight (kg)
7
+ - Height (m)
8
+ - Gender_1.0
9
+ - Experience_Level
10
+ - Difficulty Level
11
+ - Workout_Frequency (days/week)
12
+ - Water_Intake (liters)
13
+ - Fat_Percentage
14
+ - Session_Duration (hours)
15
+ - Max_BPM
16
+ - Avg_BPM
17
+ - Resting_BPM
18
+
19
+ # One-hot Workout_Type
20
+ - Workout_Type_HIIT
21
+ - Workout_Type_Strength
22
+ - Workout_Type_Yoga
23
+
24
+ # One-hot Body Part
25
+ - Body Part_Arms
26
+ - Body Part_Back
27
+ - Body Part_Chest
28
+ - Body Part_Forearms
29
+ - Body Part_Legs
30
+ - Body Part_Shoulders
31
+
32
+ # One-hot Equipment Needed
33
+ - Equipment Needed_Bench or Step
34
+ - Equipment Needed_Bench or Sturdy Surface
35
+ - Equipment Needed_Bench, Barbell
36
+ - Equipment Needed_Box or Platform
37
+ - Equipment Needed_Cable Machine
38
+ - Equipment Needed_Cable Machine or Resistance Band
39
+ - Equipment Needed_Dumbbells
40
+ - Equipment Needed_Dumbbells or Barbell
41
+ - Equipment Needed_Kettlebell
42
+ - Equipment Needed_Low Bar or TRX
43
+ - Equipment Needed_None or Dumbbell
44
+ - Equipment Needed_None or Dumbbells
45
+ - Equipment Needed_Parallel Bars or Chair
46
+ - Equipment Needed_Pull-up Bar
47
+ - Equipment Needed_Resistance Band
48
+ - Equipment Needed_Resistance Band or Cable Machine
49
+ - Equipment Needed_Step or Box
50
+ - Equipment Needed_Wall
51
+
52
+ # One-hot diet_type
53
+ - diet_type_Paleo
54
+ - diet_type_Low-Carb
55
+ - diet_type_Vegetarian
56
+ - diet_type_Keto
57
+ - diet_type_Vegan
58
+
59
+ # One-hot meal_type
60
+ - meal_type_Lunch
61
+ - meal_type_Dinner
62
+ - meal_type_Snack
63
+
64
+
65
+ features_processed_test:
66
+ - Age
67
+ - Weight (kg)
68
+ - Height (m)
69
+ - Gender_1.0
70
+ - Workout_Frequency (days/week)
71
+ # - Fat_Percentage
72
+
73
+ # One-hot Workout_Type
74
+ - Workout_Type_HIIT
75
+ - Workout_Type_Strength
76
+ - Workout_Type_Yoga
77
+
78
+ # # One-hot Body Part
79
+ # - Body Part_Arms
80
+ # - Body Part_Back
81
+ # - Body Part_Chest
82
+ # - Body Part_Forearms
83
+ # - Body Part_Legs
84
+ # - Body Part_Shoulders
src/config/inference.yaml ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Spécifications "métier" pour l'inférence
2
+ # Ces valeurs priment sur l'inférence automatique depuis X_train.
3
+
4
+ inference_full_columns:
5
+
6
+ Age:
7
+ type: integer
8
+ min: 18
9
+ max: 60
10
+
11
+ Gender:
12
+ type: string
13
+ enum: ["Male", "Female", "Other", "Prefer not to say"]
14
+
15
+ Weight (kg):
16
+ type: number
17
+ min: 30.0
18
+ max: 200.0
19
+
20
+ Height (m):
21
+ type: number
22
+ min: 1.50
23
+ max: 2.30
24
+
25
+ Fat_Percentage:
26
+ type: number
27
+ min: 11.0
28
+ max: 35.0
29
+
30
+ Max_BPM:
31
+ type: number
32
+ min: 159
33
+ max: 200
34
+
35
+ Avg_BPM:
36
+ type: number
37
+ min: 119
38
+ max: 170
39
+
40
+ Resting_BPM:
41
+ type: number
42
+ min: 49.0
43
+ max: 75.0
44
+
45
+ Session_Duration (hours):
46
+ type: number
47
+ min: 0.1
48
+ max: 6
49
+
50
+ Workout_Type:
51
+ type: string
52
+ enum: ["Cardio", "Strength", "HIIT", "Yoga", "Pilates", "Other"]
53
+
54
+ Experience_Level:
55
+ type: number
56
+ min: 1.0
57
+ max: 3.05
58
+
59
+ Workout_Frequency (days/week):
60
+ type: number
61
+ min: 1.94
62
+ max: 5.06
63
+
64
+ Difficulty Level:
65
+ type: integer
66
+ enum: [0, 1, 2]
67
+ min: 0
68
+ max: 2
69
+
70
+ Body Part:
71
+ type: string
72
+ enum:
73
+ - Abs
74
+ - Arms
75
+ - Back
76
+ - Chest
77
+ - Forearms
78
+ - Legs
79
+ - Shoulders
80
+
81
+ Equipment Needed:
82
+ type: string
83
+ enum:
84
+ - Step or Box
85
+ - Parallel Bars or Chair
86
+ - Bench or Sturdy Surface
87
+ - None or Dumbbells
88
+ - Resistance Band
89
+ - Wall
90
+ - None or Dumbbell
91
+ - Dumbbells
92
+ - Dumbbells or Barbell
93
+ - Low Bar or TRX
94
+ - Cable Machine
95
+ - Box or Platform
96
+ - Bench or Chair
97
+ - Resistance Band or Cable Machine
98
+ - Kettlebell
99
+ - Bench or Step
100
+ - Pull-up Bar
101
+ - Barbell
102
+ - Cable Machine or Resistance Band
103
+
104
+ diet_type:
105
+ type: string
106
+ enum: ["Paleo", "Low-Carb", "Vegetarian", "Keto", "Vegan", "Balanced"]
107
+
108
+ meal_type:
109
+ type: string
110
+ enum: ["Breakfast", "Lunch", "Dinner", "Snack"]
111
+
112
+
113
+ inference_test:
114
+
115
+ Age:
116
+ type: integer
117
+ min: 18
118
+ max: 60
119
+
120
+ Gender:
121
+ type: string
122
+ enum: ["Male", "Female", "Other", "Prefer not to say"]
123
+
124
+ Weight (kg):
125
+ type: number
126
+ min: 30.0
127
+ max: 200.0
128
+
129
+ Height (m):
130
+ type: number
131
+ min: 1.50
132
+ max: 2.30
133
+
134
+ # Fat_Percentage:
135
+ # type: number
136
+ # min: 11.0
137
+ # max: 35.0
138
+
139
+ Workout_Frequency (days/week):
140
+ type: number
141
+ min: 0
142
+ max: 7
143
+
144
+ Workout_Type:
145
+ type: string
146
+ enum: ["Cardio", "Strength", "HIIT", "Yoga", "None"]
147
+
148
+
149
+
150
+ # Body Part:
151
+ # type: string
152
+ # enum:
153
+ # - Abs
154
+ # - Arms
155
+ # - Back
156
+ # - Chest
157
+ # - Forearms
158
+ # - Legs
159
+ # - Shoulders
src/config/ui_defaults.yaml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Valeurs par défaut pour l'UI (Gradio)
2
+ # Ces valeurs sont utilisées pour pré-remplir les champs de l'interface.
3
+
4
+ UI_DEFAULTS:
5
+ Age: 55.92
6
+ Gender: "Female"
7
+ Weight (kg): 84.07
8
+ Height (m): 1.63
9
+ Workout_Type: "Yoga"
10
+ # Body Part: "Back"
11
+ Workout_Frequency (days/week): 3.97
12
+ # Fat_Percentage: 32.08817620708069
src/config/ui_examples.yaml ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Exemples UI affichés sous les sliders / inputs
2
+ # Chaque entrée représente un exemple complet d'utilisateur typique.
3
+ UI_EXAMPLES:
4
+
5
+ - Experience_Level: 2.01
6
+ Age: 34.91
7
+ Gender: "Male"
8
+ Weight (kg): 65.27
9
+ Height (m): 1.62
10
+ Workout_Type: "Strength"
11
+ Workout_Frequency (days/week): 3.99
12
+
13
+ - Experience_Level: 2.01
14
+ Age: 23.37
15
+ Gender: "Female"
16
+ Weight (kg): 56.41
17
+ Height (m): 1.55
18
+ Workout_Type: "HIIT"
19
+ Workout_Frequency (days/week): 4.0
20
+
21
+ - Experience_Level: 1.02
22
+ Age: 33.2
23
+ Gender: "Female"
24
+ Weight (kg): 58.98
25
+ Height (m): 1.67
26
+ Workout_Type: "Cardio"
27
+ Workout_Frequency (days/week): 2.99
28
+
29
+ - Experience_Level: 1.99
30
+ Age: 38.69
31
+ Gender: "Female"
32
+ Weight (kg): 93.78
33
+ Height (m): 1.7
34
+ Workout_Type: "HIIT"
35
+ Workout_Frequency (days/week): 3.99
36
+
37
+ - Experience_Level: 2.0
38
+ Age: 45.09
39
+ Gender: "Male"
40
+ Weight (kg): 52.42
41
+ Height (m): 1.88
42
+ Workout_Type: "Strength"
43
+ Workout_Frequency (days/week): 4.0
44
+
45
+ - Experience_Level: 1.0
46
+ Age: 53.19
47
+ Gender: "Female"
48
+ Weight (kg): 105.05
49
+ Height (m): 1.84
50
+ Workout_Type: "Yoga"
51
+ Workout_Frequency (days/week): 3.02
52
+
53
+ - Experience_Level: 3.0
54
+ Age: 23.17
55
+ Gender: "Male"
56
+ Weight (kg): 58.41
57
+ Height (m): 1.78
58
+ Workout_Type: "Strength"
59
+ Workout_Frequency (days/week): 4.96
60
+
61
+ - Experience_Level: 2.01
62
+ Age: 55.92
63
+ Gender: "Female"
64
+ Weight (kg): 84.07
65
+ Height (m): 1.63
66
+ Workout_Type: "Yoga"
67
+ Workout_Frequency (days/week): 3.97
src/gradio/__init__.py ADDED
File without changes
src/gradio/app.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import gradio as gr
3
+ import pandas as pd
4
+ import os
5
+
6
+ # ---------- Paths ----------
7
+ from .config import build_paths, UI_EXAMPLES
8
+
9
+ HERE = Path(__file__).resolve()
10
+ SRC_DIR = HERE.parents[1]
11
+
12
+ p = build_paths(SRC_DIR)
13
+
14
+ MODEL_DIR = p["MODEL_DIR"]
15
+ MODEL_PATH = p["MODEL_PATH"]
16
+ FEATURE_SCALER_PATH = p.get("FEATURE_SCALER_PATH")
17
+ TARGET_SCALER_PATH = p.get("TARGET_SCALER_PATH")
18
+ ENCODER_PATH = p["ENCODER_PATH"]
19
+ SCHEMA_PATH = p["SCHEMA_PATH"]
20
+ LOGS_DIR = p["LOGS_DIR"]; LOGS_DIR.mkdir(parents=True, exist_ok=True)
21
+ DB_PATH = p["DB_PATH"]
22
+ REPORT_PATH = p["REPORT_PATH"]
23
+
24
+ # ---------- Load model & schema ----------
25
+ from .model_loader import load_model_and_schema, load_optional_joblib
26
+
27
+ model, schema, TARGET_NAME, FEATURES, INTERNAL_EXPECTED = load_model_and_schema(
28
+ MODEL_PATH, SCHEMA_PATH
29
+ )
30
+ fx_scaler = load_optional_joblib(FEATURE_SCALER_PATH)
31
+ y_scaler = load_optional_joblib(TARGET_SCALER_PATH)
32
+ encoder = load_optional_joblib(ENCODER_PATH)
33
+ UI_FEATURE_NAMES = [f["name"] for f in FEATURES]
34
+
35
+ # ---------- Helpers ----------
36
+ from .helpers.log_utils import log_prediction
37
+ from .helpers.predict_utils import predict_single
38
+ from .helpers.schema_utils import get_bounds
39
+ from .helpers.report_utils import read_model_report, report_summary_df, report_metrics_df
40
+ from .helpers.sqlite_utils import load_val_subset
41
+
42
+ # ---------- UI ----------
43
+ def build_app():
44
+ app_title = f"TrAIn.me — (v5.0-minimal)"
45
+ app_desc_ml = "Personalize your experience"
46
+ app_desc_ex = "Choose your training program"
47
+ # app_desc_dl = "Generate your personalized exercise"
48
+ app_desc_dl_exec = "Execution generator"
49
+
50
+ from .pages.ml_tab import render_ml_tab
51
+ # from .pages.exercices_tab import render_list_of_exercices
52
+ # from .pages.dl_tab import render_dl_tab
53
+ from .pages.dl_execution_tab import render_dl_execution_tab
54
+ from .config import UI_EXAMPLES
55
+
56
+ with gr.Blocks(title=app_title) as demo:
57
+ gr.Markdown(f"# {app_title}\n{app_desc_ml} / {app_desc_dl_exec}")
58
+ with gr.Tabs():
59
+ # Onglet 1 : ML
60
+ level_out, wf_out, wt_out = render_ml_tab(
61
+ app_desc_ml=app_desc_ml,
62
+ feature_specs=FEATURES,
63
+ ui_feature_names=UI_FEATURE_NAMES,
64
+ internal_expected=INTERNAL_EXPECTED,
65
+ target_name=TARGET_NAME,
66
+ schema=schema,
67
+ ui_examples=UI_EXAMPLES,
68
+ db_path=DB_PATH,
69
+ model=model,
70
+ logs_dir=LOGS_DIR,
71
+ model_path=MODEL_PATH,
72
+ feature_scaler=fx_scaler,
73
+ target_scaler=y_scaler,
74
+ encoder=encoder,
75
+ report_path=REPORT_PATH,
76
+ on_load=demo.load,
77
+ )
78
+
79
+ # # Onglet 2 : liste des programmes
80
+ # selected_program_state, goal_state = render_list_of_exercices(
81
+ # app_desc_ex=app_desc_ex,
82
+ # level_out=level_out,
83
+ # )
84
+
85
+ # # Onglet 3 : DL – programme complet
86
+ # render_dl_tab(
87
+ # app_desc_dl=app_desc_dl,
88
+ # level_out=level_out,
89
+ # wf_comp=wf_out,
90
+ # wt_comp=wt_out,
91
+ # selected_program_df=selected_program_state,
92
+ # goal_state=goal_state,
93
+ # )
94
+
95
+ # Onglet 4 : DL – Execution generator
96
+ render_dl_execution_tab(
97
+ app_desc_dl_exec=app_desc_dl_exec
98
+ )
99
+
100
+ return demo
101
+
102
+
103
+ if __name__ == "__main__":
104
+ build_app().launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))
src/gradio/config.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pathlib import Path
3
+ import yaml
4
+ from huggingface_hub import hf_hub_download
5
+
6
+ # ---- Chaînes centralisées (sans logique) ----
7
+ # Dossiers / fichiers "métier"
8
+ MODEL_SUBDIR = ("models", "v1", "life_style_data")
9
+ # MODEL_FILENAME = "model.joblib"
10
+ # SCHEMA_FILENAME = "feature_schema.json"
11
+ REPORT_FILENAME = "model_report.json"
12
+ # Fichiers de scalers optionnels
13
+ # FEATURE_SCALER_FILENAME = "feature_scaler.joblib"
14
+ # TARGET_SCALER_FILENAME = "target_scaler.joblib"
15
+ # ENCODER_FILENAME = "encoder.joblib"
16
+ # Repo HuggingFace Models
17
+ MODEL_REPO_ID = "AIppyDev/life_style_data"
18
+
19
+ # Le notebook s’exécute depuis son répertoire → on peut repartir du cwd
20
+ current_dir = Path.cwd()
21
+ # Valeurs par défaut UI (non normalisées)
22
+ config_path = Path(current_dir / "src/config/ui_defaults.yaml")
23
+ with open(config_path, "r", encoding="utf-8") as f:
24
+ UI_DEFAULTS = yaml.safe_load(f)["UI_DEFAULTS"]
25
+
26
+ # Exemples UI (affichés sous les sliders)
27
+ examples_path = Path(current_dir / "src/config/ui_examples.yaml")
28
+ with open(examples_path, "r", encoding="utf-8") as f:
29
+ UI_EXAMPLES = yaml.safe_load(f)["UI_EXAMPLES"]
30
+
31
+
32
+
33
+ # Base de validation (relative au repo src/)
34
+ DB_RELATIVE = ("data", "processed", "life_style_data", "life_style_data_val.db")
35
+
36
+ # ---- Résolution des chemins (avec ENV overrides optionnels) ----
37
+ def build_paths(src_dir: Path) -> dict[str, Path]:
38
+ """
39
+ Construit tous les chemins nécessaires à l'app Gradio à partir de src_dir.
40
+ Les variables d'environnement suivantes peuvent override :
41
+ - MODEL_PATH, SCHEMA_PATH, MODEL_REPORT_PATH
42
+ """
43
+ model_dir = src_dir.joinpath(*MODEL_SUBDIR)
44
+
45
+ model_path_env = os.getenv("MODEL_PATH")
46
+ schema_path_env = os.getenv("SCHEMA_PATH")
47
+ report_path_env = os.getenv("MODEL_REPORT_PATH")
48
+
49
+ paths = {
50
+ "MODEL_DIR": model_dir,
51
+ "MODEL_PATH": Path(
52
+ hf_hub_download(
53
+ repo_id=MODEL_REPO_ID,
54
+ repo_type="model",
55
+ filename="life_style_data/model.joblib",
56
+ )
57
+ ) if not model_path_env else Path(model_path_env),
58
+
59
+ "SCHEMA_PATH": Path(
60
+ hf_hub_download(
61
+ repo_id=MODEL_REPO_ID,
62
+ repo_type="model",
63
+ filename="life_style_data/feature_schema.json",
64
+ )
65
+ ) if not schema_path_env else Path(schema_path_env),
66
+
67
+ "LOGS_DIR": src_dir / "logs",
68
+ "DB_PATH": src_dir.joinpath(*DB_RELATIVE),
69
+
70
+ "REPORT_PATH": Path(report_path_env) if report_path_env else model_dir / REPORT_FILENAME,
71
+
72
+ "FEATURE_SCALER_PATH": Path(
73
+ hf_hub_download(
74
+ repo_id=MODEL_REPO_ID,
75
+ repo_type="model",
76
+ filename="life_style_data/feature_scaler.joblib",
77
+ )
78
+ ),
79
+ "TARGET_SCALER_PATH": Path(
80
+ hf_hub_download(
81
+ repo_id=MODEL_REPO_ID,
82
+ repo_type="model",
83
+ filename="life_style_data/target_scaler.joblib",
84
+ )
85
+ ),
86
+ "ENCODER_PATH": Path(
87
+ hf_hub_download(
88
+ repo_id=MODEL_REPO_ID,
89
+ repo_type="model",
90
+ filename="life_style_data/encoder.joblib",
91
+ )
92
+ ),
93
+ }
94
+ return paths
src/gradio/data/corpus_car_FULL_1.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ l'objectif de cette vidéo c'est donc que vous trouviez la meilleure organisation pour vos training et je m'adresse à desgâts naturels en tant que mec naturel et de toute façon il y a que des mecs chargés pour dire oui mais chargé pas chargé c'est pareil pour tout le monde si c'était le cas vous auriez pas besoin de vous charger on va voir dans cette vidéo de la pire façon d'organiser ses entraînements à la meilleure en gros comment diviser vos training pour être le plus efficace possible je vais classer les différents styles de division d'entraînement les plus courants et utilisés en muscu à savoir le body qui est une technique qui vise à travailler l'ensemble du corps au sein de la même séance le half body avec le half body on divise le corps en deux en alternant en général une séance upper haut du corps et une séance lower bas du corps le PPL c'est une division en trois parties un push les muscles qui servent à pousser essentiellement les pecs triceps deltoïdes faisau moyen et antérieur poule les muscles qui servent à tirer donc essentiellement le dos les BPS deltoïde faisau postérieur et les jambes bien sûr qui regroupent les cuisses les fessiers et les mollet ensuite on verra le mix entre ALP et PPL donc vous l'avez compris comme son on indique c'est un mix des deux et pour terminer bien sûr le split c'est un terme un peu détourné parce que ça veut dire juste diviser mais dans le monde de la muscu on s'accorde à dire que c'est une division en général en 5 ou plus exemple de Split une s spec puis dos puis jambes puis épaule puis bras ça peut même aller plus loin dans la division mais on va garder celle-ci qui est la courante et bien sûr ça n'aurait aucun sens de faire ce classement si je prenais pas en compte le nombre de jours d'entraînement que vous faites dans la semaine donc vous allez voir que si vous vous entraînez une fois par semaine ou SEP fois c'est pas du tout les mêmes styles d'entraînement qu'il faut privilégier et pour terminer il y en aura pour tout le monde peu importe le nombre de jours où vous allez à la salle vous saurez après cette vidéo exactement comment diviser vos entraînements ok les gars on commence donc avec une séance par semaine pour introduire le sujet on sait que il faut stimuler le muscle d'accord deux fois par semaine pour avoir une progression optimale maintenant quand vous vous entraînez une fois par semaine bah vous pouvez pas stimuler chaque muscle deux fois vous comprenez bien mais une fois c'est OK une fois ça vous permet quand même de faire de la musculation d'avoir des résultats et ceux qui disent oui vaut mieux pas y aller que s'entraîner qu'une fois non moi je fais du paddle une fois par semaine j'ai pas un gros niveau ni de grosses ambitions pour autant à chaque fois que j'y vais je m'améliore un petit peu je prends plaisir et je suis meilleur qu'un gars qui n'y va pas du tout la progression oui elle est là mais elle sera lente la récupe bah elle sera très bonne vous avez pas à vous soucier de ça et juste avant de vous donner mon class si déjà vous vous engagez à aller à la salle une fois par semaine et que vous vous y tenez peut-être que par la suite vous allez pouvoir rajouter des séances et continuer à progresser un peu plus vite n'oubliez pas que en musculation ça sert à rien de s'y mettre à fond d' aller 2 heures par jour de faire des séances interminables de tout calculer au gram près si vous tenez pas sur la durée nous c'est ce qui a fait nos résultats c'est le fait de s'entraîner de manière régulière et de manière intense on est loin d'être ceux qui s'entraînent le plus de fois par semaine ni le plus longtemps pourtant on a des bons physiques bien sûr encore une fois pour pour terminer une séance par semaine vous allez pas avoir un physique de statue grec maintenant quelle est la meilleure répartition de quelle manière diviser ces entraînements quand on fait qu'une séance par semaine la pire de toutes bien sûr c'est le split parce que le split vous allez entraîner donc disons allez un split classique division en 4atre où on va faire les pecs avec les triceps le dos avec les biceps et cetera si vous diviser par 4 ça veut dire que chaque groupe musculaire vous allez le faire une fois par mois vous imaginez bien que là au niveau progression niveau résultat ça va pas être phénoménal donc c'est le pire ensuite vous avez le PPL pourquoi parce que c'est une division en trois parties donc vous allez faire chaque muscle une fois toutes les 3 semaines ensuite juste au-dessus très très proche vous avez half body plus PPL c'est quasi similaire ensuite vous avez le half body et là ça devient déjà un peu plus intéressant parce que chaque zone on va la travailler au moins une fois toutes les deux semaines et bien sûr le top du top si vous faites qu'une séance par semaine c'est le full body l'ensemble du corps parce que bien sûr bah vous allez au moins stimuler une fois chaque groupe musculaire dans la semaine ok on enchaîne maintenant avec deux séances par semaine déjà ça commence à être plus intéressant il y a une chose qui est importante si vous savez que vous pouvez aller à la salle deux fois par semaine si possible au moins d'accord un jour de repos entre les deux séances comme ça vous allez pouvoir travailler les mêmes zones musculaires deux fois dans la semaine si vous les mettez deux jours d'affilé malheureusement vous avez pas la récupération optimale pour refaire une même séance donc prenez au moins un jour de repos deux séances par semaine encore une fois et ben c'est mieux que une qui est moins bien que trois donc deux séances par semaine vous aurez deux fois plus de résultats pratiquement que la personne qui va y aller qu'une fois c'est encore une fois pas assez pour avoir un physique de statut grec d'accord note possible ok à moins que vous fassiez un autre sport qui va travailler de manière intense on va dire le physique vous pouvez quand même avoir un petit niveau voilà avec une bonne diète et cetera mais vous n attendez pas à des folies maintenant quel méthodologie quelle planification d'entraînement privilégié quand vous allez deux fois par semaine à la salle si vous avez bien écouté le split malheureusement encore une fois c'est la même division on va faire de séances par semaine donc on va attendre longtemps avant de retravailler le même muscle au minimum de semaines voire 3 semaines donc le split c'est le pire de tous juste au-dessus c'est pareil c'est le PPL vous avez aussi le mix ALP PPL juste au-dessus vous avez le half body qui est plutôt pas mal parce que vous allez bien mettre le focus et pouvoir faire un gros volume d'entraînement sur la zone qui est travaillée mais par contre vous allez la stimuler qu'une fois donc le half body c'est plutôt pas mal si vous faites vos deux jours d'affilé si par exemple vous entraînez mardi mercredi et ben vous savez que le half body c'est la meilleure solution maintenant si vous pouvez mettre au moins un jour de repos faites du full body et essayez de faire des séances qui sont plutôt longues avec des gros volumes d'entraînement bien sûr c'est du full body on va pas rester non plus 3h à la salle mais des séances d' 1h30 en full body en faisant au moins deux exercices pour les gros groupes musculaires et un exercice pour les petits groupes musculaires c'est ce qui me semble être le plus judicieux on passe aux choses sérieuses trois séances par semaine déjà avec trois séances par semaine on peut avoir un très bon physique déjà ça c'est une chose qu'il faut vous mettre en tête ça devient vraiment sérieux quand on est vraiment assidu qu'on s'entraîne de manière intelligente régulière avec de l'intensité en trois séances par semaine on peut vraiment avoir un très bon physique il y en a plein qui vont penser que le PPL c'est le plus adapté pour trois séances par semaine parce que bah justement c'est divisé en trois on va faire une séance push une séance poule une séance legs c'est parfait maintenant c'est pas la meilleure des répartition parce queon l'a vu tout à l'heure c'est mieux d'avoir plus de stimuli que ça dans la semaine du coup ce que je vous recommande si vous pouvez vous entraîner trois fois par semaine c'est de placer vos jours de repos entre chaque séance au moins un jour de repos entre chaque séance pour pouvoir retravailler le même muscle la séance suivante ou au moins travailler plusieurs fois les mêmes muscles dans la semaine donc si par exemple vous pouvez mettre lundi mercredi vendredi des entraînements c'est parfait et vous me voyez venir donc oui bien sûr la moins bonne des divisions ça va être le split encore une fois parce que on fait pas assez d'entraînement dans la semaine pour pouvoir stimuler de manière efficace chaque zone musculaire le pire c'est le split juste au-dessus vous avez le PPL juste au-dessus vous avez le half body plus PPL là où ça commence à être intéressant bah c'est le half body et pourquoi ça commence à être intéressant parce que une semaine sur deux vous allez faire la moitié du corps deux fois et l'autre moitié une fois et l'autre semaine et ben ça sera l'inverse la moitié du corps que vous avez fait deux fois la première semaine vous allez la faire une fois et celle que vous aviez fait une fois vous allez la faire deux fois donc c'est pas mal on va dire c'est une moyenne de 1,5 stimuli par muscle maintenant si vous voulez avoir chaque muscle stimulé deux fois le top du top c'est le full body avec bien sûr à chaque fois au moins un jour de repos entre chaque séance et c'est là vous allez avoir les résultats les plus optimaux quatre séances par semaine la marque des grands hommes la marque des hommes puissants bizarrement c'est à peu près le nombre de séances que moi je m'entraîne par semaine moi je vous avoue la vérité c'est que moi je vise 5 séances par semaine mais que il y a toujours un imprévu qui fait que en moyenne je tourne plus autour de 4 donc déjà avec qu sen par semaine ça devient vraiment sérieux d'accord vous pouvez avoir une grosse marge de progression ce qui est bien avec quatre séances par semaine c'est que vous pouvez plus varier vos entraînements ce qui est bien avec qu sances par semaine c'est que vous vous instaurer une vraie routine c'est ancré dans votre Lifestyle pratiquement plus de la moitié on va dire de la semaine vous allez à la salle donc quand vous le faites sur le long terme ça va vous apporter beaucoup de bénéfices d'accord dont la discipline et tout ce qui en découle motivation confiance en soi et cetera maintenant c'est là où ça va changer au niveau de la planification quatre séances par semaine bon vous savez à quel point je suis pas un fan du split et c'est pas pour faire le forceur mais même en faisant quatre séances par semaine le split c'est c'est pas ce qu'il y a de plus rentable pourquoi parce que quand vous faites du split vous divisez votre corps en quatre parties minimum donc si vous avez quatre séances par semaine vous stimulez chaque zone qu'une fois en général les mecs ils aiment bien faire PEC triceps dos biceps épaule jambes parfois même divisé quadr isquio bref le problème c'est qu'il y a pas assez de stimuli donc oui on peut mettre un gros volume d'entraînement sur la zone travaillée mais on le sait si c'est scientifiquement prouvé il vaut mieux avoir un volume d'entraîn plus faible mais stimulé deux fois la zone dans la semaine que de faire une séance interminable par exemple sur les pecs au final ça devient contreproductif donc le split je le mets encore en dernier et c'est la dernière fois vous inquiétez pas ensuite juste au-dessus et ben ça va être le full body l'autre extrême pourquoi parce que stimuler chaque zone quatre fois dans la semaine ça devient trop vous avez pas assez de récupération au final il y a trop de stimuli et pas assez de volume d'entraînement par groupe musculaire dans votre séance donc c'est pas on va dire la plus rentable il y a des gens qui le font même jusqu'à 7 fois vous pouvez faire du full body en fait tout se fait tout peut fonctionner mais le but et ce que je vous explique depuis tout à l'heure c'est le chemin le plus rapide en fonction du nombre de séances que vous avez à disposition le but c'est d'arriver à Rome le plus vite possible tous les chemins nous mènent à Rome maintenant nous on veut pas y aller par national on veut prendre l'autoroute donc split full body on laisse tomber maintenant là où ça devient intéressant c'est le pushpol legs donc le push pull legs ça devient déjà un peu plus intéressant parce queà chaque fois il va y avoir une des zones soit les muscles qui vous servent à pousser soit ceux qui vous servent à tirer soit les jambes qui vont être stimulés deux fois et à chaque semaine c'est un nouveau groupe musculaire qui est stimulé deux fois malheureusement ça reste trop faible d'accord il y a pas assez quand même de stimuli et on passe donc au half body mixé au PPL qui devient encore un peu plus intéressant parce que vous allez la plupart du temps stimuler deux fois chaque groupe musculaire d'accord si je fais upper lower PPL l si vous y allez quatre fois d'accord ça fait sence 1 2 3 4 et 1 2 3 4 et ainsi de suite vous voyez cette semaine là j'ai fait les muscles du haut du corps une fois push Pol deux fois donc ça c'est toujours le haut du corps ils ont été stimulés deux fois bref c'est intéressant parce que la plupart du temps vous allez stimuler deux fois chaque groupe musculaire mais à chaque semaine il y a un groupe qui est toujours un peu moins travaillé que les autres donc bien sûr vous l'avez compris le top du top quand vous faites quatre séance par semaine c'est de vous entraîner en half body pourquoi parce que vous allez avoir un volume d'entraînement conséquence sur chacune des zones et vous allez la stimuler cette zone deux fois dans la semaine ensuite les gars on enchaîne avec C séances par semaine c'est le top du top c'est ce que j'aimerais bien m'entraîner mais en même temps en même temps il faut laisser un petit peu respirer la concurrence faut leur donner un petit peu de la force donc je m'arrête à 4 mais 5 sces par semaine vraiment pour des gars qui ont un niveau en musculation qui s'entraînent de manière régulière Adu intense vous pouvez avoir des tops physiques avec ça des physiques de fou furieux des physiques de de statut grec et ce qui est bien en plus quand vous entraînez 5in fois par semaine c'est qu'il y a plusieurs divisions qui peuvent être intéressantes donc à vous aussi de tester de toute manière tout ce que je vous dis c'est basé encore une fois sur la science sur mon expérience mais vous devez aussi prendre en compte vous-même vos ressentis vos préférences ce qui marche sur vous donc c séances par semaine d'accord physique normalement d'accord si vous vous entraînez bien et depuis longtemps vous avez un physique de fou furieux d'accord si c'est pas le cas et que vous entraînez depuis plusieurs années 5 séances par semaine que vous mettez de l'intensité et cetera vous voyez vite la façon dont vous vous entraînez il y a quelque chose qui cloche venez faire un tour dans la barre d'info il y a le programme Apollon si toi aussi tu veux te transformer en 3 mois comme les gars que tu vois là et avoir un programme d'entraînement sur mesure la diète sur mesure et bien sûr les recettes qui vont avec avec le suivi des coachs nous-mêm sur WhatsApp tu as le lien en barre d'info c'est le programme qui a transformé le plus d'hommes en France alors pourquoi pas toi un autre point positif quand vous y allez 5 séance par semaine vous êtes quelqu'un à la salle vous êtes quelqu'un c'estàdire que quand vous arrivez on vous app par votre prénom il y a pas de carte qui passe pas tout le monde vous connaît vous dites bonjour à tout le monde et cetera vous savez quand il y a un nouveau bref vous faites partie des machines pas des meubles des machines et bien sûr on en a parlé juste avant quand tu t'entraînes cinq fois par semaine tu sais t'imposer une autodiscipline et ça c'est quelque chose qui vraiment va te servir dans la vie de tous les jours parce que tu sais être résilient d'accord quand tu y vas CIN fois par semaine c'est que tu y vas peu importeon ton niveau de motivation donc tu as le mindset j'ai fait trois traits d'accord je sais pas pourquoi maintenant il faut que je trouve la troisième raison mais bref quand tu es autodiscipliné tu es tout simplement confortable ah là vous croyez que j'allais pas trouver tu es confortable parce que quoi qu'il arrive peu importe le confort l'inconfort tu y vas tu es autodiscipliné oui ça se répète et alors on va voir maintenant quelle est la meilleure répartition quand on s'entraîne 5 fois par semaine la moins bonne des divisions vous l'avez compris ça va avec ce que j'ai dit juste avant mais c'est le full body parce que il y a trop de stimuli CIN fois par semaine c'est trop ensuite vous avez le split et oui pour tous les grands fans de Split qui étaient en transpiration depuis tout à l'heure et qui attendaient que j'annonce que c'était la meilleure méthode quand on s'entraîne 5in fois par semaine et ben non c'est toujours pas le split pourquoi parce que la plupart des zones ne vont être stimulées encore une fois qu'une fois dans la semaine donc c'est la deuxième moins bonne ensuite à égalité vous avez le push pull legs et puis le body le pushpol leg ce qui est bien c'est que la plupart du temps les zones vont être stimulées deux fois sauf une des zones dans la semaine et le half body le problème c'est que il y en a une qui va être stimulée trois fois ce qui est peut-être un petit peu beaucoup vaut mieux augmenter peut-être le volume d'entraînement bref ça se discutait un petit peu sur les deux mais par contre le top du top comme division si vous entraînez CIN fois par semaine c'est de faire un mix justement entre PPL et half body et c'est exactement de cette manière que moi je structure mes entraînements et vous allez comprendre pour pourquoi si séances par semaine j'ai pas effacé ça parce que si séances par semaine si tu as pas un physique de fou furieux là ça devient très grave si séances par semaine l'autodiscipline c'est même plus une question si séances par semaine tu es pas quelqu'un à la salle tu es un actionnaire d'accord tu tu contribues vraiment à l'économie du club donc bravo à toi et si séances par semaine c'est quelque chose pour les passionnés si tu es pas un passionné si tu prends pas plaisir à aller à la salle si tu trouves pas en tout cas les bénéfices et au bout de un moment tu vas lâcher parce que si Séan par semaine c'est c'est du temps je sais pas si vous vous rendez compte c'est peut-être une journée dans toute ta semaine pratiquement que tu as passer à la salle parce que on connaît les les mecs qui vont six fois par semaine qui discutent pendant 2 Hees et cetera bref si séances par semaine tu es obligé d'être une machine de guerre et quelle est la meilleure manière de s'entraîner déjà la pire en 6 s par semaine c'est le full body toujours pour la même raison juste au-dessus le half body parce que tu vas faire trois fois chaque zone par semaine c'est beaucoup trop et là les gars sont là il se disent bon c'est bon maintenant il va nous dire que c'est le split ou pas et ben non le split il arrive maintenant c'est pas toujours pas la meilleure pourquoi parce que tu vas encore une fois stimuler certaines zones que une fois dans la semaine donc non le split n'est pas la meilleure méthode même si tu t'entraînes six fois dans la semaine mais vous inquiétez pas c'est pas déconnant je veux dire si vraiment ton kiff c'est de faire du split et tu vas à la salle six fois par semaine fais du split tu auras des très bons résultats juste au-dessus vous avez la division hybride que j'adore al body PPL mais qui n'est pas non plus la meilleure parce que la meilleure si vous y allez six fois par semaine c'est de vous entraîner en pushpol legs x 2 et je vous conseille de faire des séances différentes c'estàdire d'avoir deux séances push de séances pool et de séances legs différentes si vous y allez six fois par semaine et on enchaîne avec 7 séances par semaine quand tu vas à la salle SEP fois par semaine tu te dois d'avoir un physique de Alpha d'accord ça commence comme ça MGA giga ultra Chad je sais pas ce que ça veut dire Chad mais vous avez compris et tu es plus actionnaire du club tu fais la bise au patron là si tu fais pas la bise il y a quelque chose qui va pas tu vas cette fois à la salle tu habites là-bas mon gars tu dois faire la bise au patron tu dois lui quand il part il sort de la salle tu dois lui mettre une petite fessée il y a quelque chose tu vois l'autodiscipline tu la bois l'autodiscipline c'est une blague pour toi quand tu vas cette fois à la salle par semaine maintenant c'est quoi la meilleure planification quand tu vas cette fois la salle par semaine allez-y allez-y c'est bon vous êtes contents la pire de toutes le full body juste au-dessus le body encore au-dessus l'hybride al plus PPL juste au-dessus ou là là attention le suspect est-ce qu'il a mis avant est-ce qu'il a mis après juste au-dessus nous avons le PPL parce que oui il va y avoir une zone qui va être stimulée trois fois ce qui peut faire beaucoup surtout quand on prend pas de jours de repos donc le top du top finalement quand on s'entraîne 7 jours sur 7 c'est de faire du split et je vais même vous l'écrire en gros ils sont contentsok ok maintenant qu'on a fait ce tableau on va voir quelle est la planification gagnante donc déjà la perdante la toute première des perdantes et c'est pas un secret on vous le dit depuis des années c'est le split malheureusement le split est efficace et intéressant uniquement selon moi si vous entraînez plus de 5 fois par semaine sinon il y a mieux je dis pas que c'est nul mais sinon il y a mieux donc le split une note de 2,14/ 5 ensuite vous avez le full body tant qui est une technique que j'apprécie et je pousse beaucoup de person person à faire du full body notamment quand il s'entraîne moins de quatre fois par semaine mais le full body voilà a eu la note de 3,14/ 5 parce que oui à partir du moment où on veut vraiment un bon niveau et aller à la salle au moins quatre fois par semaine ça devient beaucoup trop ensuite vous avez le PPL donc push pool legs qui est je pense actuellement la méthode de musculation la plus suivie en salle de sport et c'est pas une mauvaise chose le PPL c'est très bien c'est juste que ça manque un petit peu de stimuli si vous y allez pas six fois dans la semaine et on sait très bien que même ceux qui doivent y aller six fois mais il y a souvent des jours qui sautent donc le PPL 3,42/ 5 ensuite à égalité vous avez le al body et la division hybride entre half body et PPL qui monte à 3,57/ 5 maintenant pourquoi j'ai fait ce classement parce que c'est super intéressant dans le sens où en fait tu t'organises en fonction du nombre de séances que tu sais que tu vas pouvoir faire et il faut réussir à s'organiser à anticiper ce que vous avez à faire dans la semaine pour justement avoir une planification qui sera la plus efficace maintenant si vous êtes pas certain de toujours pouvoir faire le même nombre de séances alors faites comme moi et c'est exactement ce que j'ai fait en faisant cette division hybride al body PPL parce que si vous entraînez que trois fois et ben vous allez faire du al body si vous savez que vous entraînez que trois fois vous faites du al body si vous savez que vous allez vous entraîner quatre fois dans la semaine pareil vous faites les deux séances half body vous les répétez si vous savez que vous allez y aller cinq fois et ben vous faites votre division half body PPL et si vous savez que vous y allez six fois vous faites le PPL deux fois j'espère que vous avez compris ma logique pour moi c'est de loin la division la plus adaptable et la plus efficace et celle qui correspond à mon lifestyle à mon changement d'organisation de planning et cetera maintenant vous vous demandez peut-être c'est quoi le plus rentable de tout quel est le type d'entraînement où je vais passer le moins de temps possible et qui va me fournir le plus de résultats alors si vous vous posez cette question les gars quelle est la division qui va vous donner le plus de croissance muscul en passant un minimum de temps à la salle par semaine la réponse c'est le al body en quatre séances et d'ailleurs vous avez un ebook avec un programme qu'on vous offre et je peux vous assurer que si vous savez pas comment vous entraîner et diviser vos entraînements ou si alors vous vous êtes rendu compte que ce que j'ai présenté mais ça correspondait pas à ce que vous aviez mis en place essayez ça et vous allez nous en dire des nouvelles les gars j'espère en tout cas que la vidéo était précise que vous l'avez bien comprise que vous êtes d'accord avec nous si c'est pas le cas vous pouvez très bien bien débattre dans les commentaires et c'est avec plaisir pour vous lire c'est basé bien sûr sur notre expérience en tant que coach en tant que pratiquant mais aussi sur les études scientifiques et les études que nous on a pu faire de notre côté je ferai d'autres classements si ça vous intéresse de ce style dites-moi en commentaire ce que vous voulez que je classe mais bien sûr à chaque fois je faire en sorte que ce soit pertinent comme là c'est pas pertinent si on n'associe pas le nombre de jours d'entraînement qu'on place dans la semaine donc oui faire un classement par groupe musculaire d'exercices les plus efficaces si on prend pas tous les facteurs en compte ça n'a pas d'intérêt donc si je la fais je le ferai en fonction de certains facteurs pensez à télécharger le programme gratuit si vous voulez une grosse transformation en 3 mois vous savez où ça se passe c'est en barre d'info j'espère en tout cas que vous avez aimé la vidéo si c'est le cas pensez à liker si c'est pas le cas vous êtes quand même des gros teb d'être arrivé jusqu'ici c'était alex je vous dis à très vite et surtout d'ici là gardez la pêche
src/gradio/data/corpus_car_FULL_2.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ début 2025 explosion des ventes d'abonnements dans les salles de fitness c'est là où ils font leurs plus gros chiffres et les salles sont blindées en se début janvier et dans un mois ce sera vide donc dans cette vidéo on va voir comment obtenir son meilleur physique et surtout comment tenir sur la durée ses engagements pour réussir sa transformation bon avant de commencer on va voir toutes les erreurs qui vous font tout simplement abandonner vos bonnes résolutions et qui sont très communes à toutes les personnes qui abandonne la première c'est d'aller à la salle sans plan d'entraînement d'y aller au hasard en vous disant bah je verrai bien sur place ce que je vais faire non quand vous allez à la salle vous devez avoir un programme structuré savoir exactement ce que vous allez faire combien de temps ça va vous prendre on laisse pas de place à l'inconnu plus vous planifiez plus vous organisez plus vous allez tenir deuxème erreur très commune commencez en mettant trop d'intensité trop d'exercices trop de séances dans la semaine des séances trop longue n'oubliez pas une chose vous commencez vous êtes au début vous allez pas avoir le même programme d'entraînement que quelqu'un qui s'entraîne depuis 10 ans donc allez-y progressivement sinon vous allez vous en dégoûter vous allez avoir des courbatures incroyables bref c'est la meilleure manière pour abandonner vite rappelez-vous une chose votre but c'est d'avoir un bon physique et de l'avoir sur la durée de continuer à progresser de continuer à l'avoir et d'avoir ce physique tout au long de l'année si vous partez trop fort vous allez abandonner erreur numéro 3 et là ça concerne la nutrition deux grosses erreurs souvent les personnes qui sont trop minces et qui veulent prendre du poids vont partir vers le Dirty bulking ce qu'on va dire c'est manger un maximum de calories pour prendre du poids et au contraire il va y avoir tous ceux qui sont en surpoids qui ont trop de masse grasse qui vont vouloir perdre du poids et se mettre en gros déficit calorique ce sont deux erreurs communes qu'il ne faut pas que vous fassiez d'une pourquoi parce que si vous êtes en trop gros déficite calorique vous allez avoir un métabolisme qui va être ralenti et du coup vous allez consommer moins de gras et c'est donc contreproductif et pour le Dirty bulking c'est-à-dire ceux qui vont manger un maximum de calories pour prendre du poids c'est comme faire CIN pas en arrière pour faire un pas en avant vous ce que vous voulez c'est du muscle c'est pas spécialement de la masse grasse donc avoir un léger surplu calorique ok mais le Dirty bulking ne vous servira à rien vous allez juste B faire du gras que vous allez galérer à enlever après 4è erreur et là malheureusement vous ne pouvez rien y faire sauf y faire abstraction c'est les conseils de tout le monde et surtout de n'importe qui à la salle tout le monde veut donner des conseils tout le monde est un expert on entend des versions sur tel et tel sujet à dormir debout dans tous les cas focalisez-vous sur quelqu'un qui a des compétences comme par exemple Alex de bodytime je dis ça comme ça d'accord des professionnel et suivez votre entraînement votre planification allez jusqu'au bout ne posez pas des questions essayez pas de tout tester en même temps dernière erreur et qui est aussi très commune c'est de donner trop d'importance au compléments alimentaires ce sont des compléments alimentaires on appelle ça aussi suppléments alimentaires c'est en complément c'est en supplément c'est-à-dire que c'est en plus de l'entraînement l'alimentation le le sommeil le lifestyle tout ça c'est plus important que la prise de compléments alimentaire donc encore une fois si vous en avez pas vous voulez pas en prendre dès le début pas de souci vous pouvez très bien réussir votre transformation physique et si vous en prenez dites-vous que c'est juste un plus on va voir maintenant le point de départ j'aurais très bien pu faire une vidéo en vous disant ben ok fais ça mange comme ça entraîne-toi comme ça va à la salle tant de fois par semaine sauf que ben ça serait au mettre votre niveau de départ et la situation dans laquelle vous êtes alors que c'est primordial de faire justement un état des lieux de votre physique tout en étant honnête mettez votre ég de côté regardez-vous dans le miroir et soyez honnête est-ce que je suis gras d'accord c'està dire en surpoid est-ce que j'ai de la masse graisseuse entreut voilà fat entre guillemets est-ce que je suis plutôt skinny c'est-à-dire que je suis fin j'ai pas beaucoup de masse musculaire je suis pas bien épais et c'est ce qui me gêne ou alors est-ce que je suis un mix des deux c'est-à-dire fin avec pas beaucoup de masse musculaire mais par contre de la masse graiss ce qu'on appelle les skinny fat ça c'est le point de départ vous devez déterminer le physique que vous voyez dans la glace pour justement adapter ce que vous allez mettre en place on enchaîne maintenant avec les trainings et pourquoi on enchaîne avec les training parce qu'en fait que vous soyez fat skinny ou skinny fat c'est la même chose vous allez devoir vous entraîner et les conseils que je vis vous donner pardon que je vont vous donner que je vais vous donner s'applique à tout le monde sur les réseaux et d'une manière générale sur Internet et même à la salle vous vous voyez des dizaines des centaines des milliers d'exercices plus farfelus les uns que les autres il y en a pour tous les goûts et ça peut encore paraître bizarre parce que moi c'est vrai que j'aime bien faire des exercices qui sortent de l'ordinaire mais je suis un pratiquant de longue date et j'ai besoin d'amener régulièrement bah de la nouveauté du fun de Me challenger et d'être le plus complet possible donc oui je vais faire plein d'exercices qui sortent de l'ordinaire mais si vous êtes un débutant si vous êtes au début de votre transformation physique restez sur les exercces classiques les exercices polyarticulaires ceux qui vont vous faire consommer un maximum de calories travailler le plus de muscles à la fois ce qu' faut entrer plusieurs articulations en compte donc comme le développer couché le rowing les tractions les squat le soulever de terre les épaules et jetées les développer épules développer militaire des fentes et cetera ce sont des exercices que vous devez privilégier et surtout bien travailler votre technique bien travailler le mouvement avant de de passer à des exercices plus complexes bon peut-être que j'ai dit beaucoup d'exercices et que ça vous a perdu mais vous inquiétez pas vous n'avez pas besoin de retenir tous ces noms d'exercice on vous a préparé un programme débutant qui est gratuit le lien il est en barre d'info vous avez juste à le télécharger et à vous laisser guider à votre niveau la chose la plus importante c'est d'être maintenant régulier c'est pas de faire des séances de fou de rester 2hes à la salle d'y aller SEP fois par semaine non c'est de commencer avec un petit engageement que vous savez que vous allez tenir comme par exemple trois séances par semaine vous restez à la salle maximum une heure pour commencer c'est déjà très bien et vous allez avoir des courbatures vous allez avoir des résultats en commençant comme ça on dit il vaut mieux s'entraîner bien régulièrement que très bien de temps en temps un autre point qui est très important c'est pas directement relié à votre muscle mais ça va avoir un impact sur votre progression musculaire c'est l'environnement dans lequel vous vous entraînez il faut que vous soyez à au début les premiers jours c'est possible que vous soyez mal à l'aise mais si ça dure alors il faut trouver un autre environnement où vous allez être à l'aise si la salle ne vous convient pas changez de salle si vous entraînez en salle ça vous convient pas entraînez-vous chez vous entraînez-vous à l'extérieur changez de pratique bref trouver un environnement où vous allez vous sentir bien trouver des amis avec qui alleer partager vos entraînements des groupes peut-être sur les réseaux sociaux rejoignez-nous aussi pour vous aider à vous motiver bref soyez dans un environnement propice à la progression quelques petits conseils aussi il y en a certains pour qui ça peut avoir un gros impact moi je sais que c'est pas spécialement mon cas mais pour beaucoup d'autres personnes ça aide achetez-vous une tenue dans laquelle vous êtes à l'aise vous avez envie de la mettre ça vous fait plaisir quelques petits accessoires pour aller à la salle qui vous font plaisir une belle gourde une serviette une nouvelle tenue une nouvelle paire de basket bref ne vous focalisez pas là-dessus c'est pas ça qui va faire votre transformation mais si ça peut rajouter du plaisir ça vous permettra d'être plus régulier et surt surtout de pas lâcher moi par exemple avant de mettre des débardeurs bodyt je faisais 20 kg de moins à partir du moment où j'ai eu ce débardeur bodyt là j'avais envie d'aller à la salle tous les jours et pour récapituler rapidement sur la partie training ce qu'il faut c'est faire des exercices simples du polyarticulaire rester dans des fourchettes de répétition aussi qui vous correspondent quand vous êtes débutant c'est plutôt entre 10 et 15 répétitions on peut descendre quand on est plus avancé et dernier point important si possible pour une progression optimale stimuler deux fois chaque groupe musculaire par semaine mais encore une fois vous avez le programme gratuit en barre d'info ok maintenant on va voir la nutrition et c'est à ce moment-là où c'est important de prendre en compte le fait que soit vous êtes fat soit vous êtes skinny soit vous êtes skinny fat pour commencer si vous êtes gros bon si vous êtes fat ça passe mux en anglais on a le droit de le dire en anglais en français à ce qu' paraît pas trop c'est méchant la première chose à faire c'est pas de tout traquer de compter votre gramme de riz grain par grain à moins que vous rentriez dans un programme de transformation physique et vra vous vous mettiez à fond dedans et cetera mais sinon il faut juste regarder ce que vous êtes en train de manger prendre conscience chaque jour de ce que vous mangez ce que vous buvez et le contrôler en essayant d'éliminer la junk food d'éliminer les boissons gazeuses les boissons sucrées et tout en fait ces calories vides qui nous apportent rien pas d'effet de saciété ça vous apporte aucun bénéfice d'un point de vue santé d'un point de vue performance d'un point de vue musculation tout simplement donc privilégie bien sûr tous les aliments non transformés le moins possible manger des aliments en fait tout simplement brutes que vous allez cuisiner vous-même pourquoi je vous dis ça parce que dans les aliments transformés et ultra transformés il y a énormément d'additifs et de sucre et de gras que ce soit pour la conservation pour le goût et bien sûr pour vous rendre accro et vous faire continuer à consommer ces merdes vous allez voir que tout simplement en faisant ça vous allez voir votre poids descendre sur la balance et votre masse grasse diminuer et dans tous les cas si vous avez 20 % de masse grasse ou plus que ça en changeant seulement ça vous allez voir de gros Chang c'est sûr que si vous êtes plus à 15 % de masse grasse que vous êtes un peu gras vous avez pas les abdos mais vous êtes pas en gros surpoids il va falloir fournir sûrement plus d'effort on va voir maintenant les skinny les maigres maigres j'ai le droit de le dire c'est pas c'est pas une insulte c'est gros qu'on a pas le droit maig pour vous ce qu'il faut prioriser c'est le fait de vous entraîner plus dur à chaque fois je disais tout à l'heure pas trop intense mais vous vous avez quand même besoin de mettre une certaine intensité pour créer du muscle maintenant au niveau de la nutrition il faut prioriser les prot proté vous devez manger assez de protéines et jusqu'à 2 g par poids de corps pour faire du muscle quand vous vous entraînez vous cassez des fibres musculaires et grâce à ces protéin pour faire simple vous reconstruisez ses fibres musculaires un petit peu plus fortes donc c'est pour ça que vous devez vous entraîner un peu plus dur à chaque fois et avoir un bon apport en protéin jusqu'à 2 g par poids de corps maintenant le souci c'est que quand vous mangez beaucoup de protéines vous pouvez peut-être ne pas arriver au ratio calorique dont vous avez besoin c'est-à-dire ne pas manger assez de calories au total et du coup vous allez pas prendre de poids donc l'astuce pour manger manger assez de calories c'est de manger beaucoup de smoothie des purées et tous ces aliments là que vous avez pas besoin de mastiquer pourquoi parce que la mastication va envoyer un effet de saciété au cerveau plus rapidement donc si vous mangez des purées et des soupes vous mâchez moins vous envoyez cet effet de saciété plus tard donc vous mangez plus tout ça en fait c'est le principe de densité calorique vous devez manger des aliments qui en peu de volume vous apporte beaucoup de calories en plus de ça bien sûr privilégiez toujours les aliments organiques les aliments réel et pas transformé donc dans mon smoothie je vais mettre des bananes des amandes du beurre de cacahuète tout ça ce sont des aliments très caloriques qui vont vous permettre d'atteindre votre quota calorique en vous donnant bah des nutriments de bonne qualité et bah sans vous détruire la santé tout simplement maintenant si vous êtes skiny fat il y a deux options soit vous partez dans l'option prendre du poids pour faire du muscle parce que c'est vraiment ça qui vous gêne le plus dans votre physique mais moi je préfère l'option de sécher de commencer avec un lait g déficit calorique parce que vous allez tout de suite avoir l'impression d'avoir plus de muscles parce que la définition vous donne cette impression là quand vous perdez du gras vous voyez mieux vos muscles et parfois peut-être que vous vous trouviez maigre mais en perdant cette petite couche de masse grasse mais en fait vous avez une masse musculaire qui va se dévoiler et vous allez vous trouver bien mieux esthétiquement parlant alors oui peut-être que dans les vêtements vous allez pas bien plus les remplir mais c'est déjà une première étape qui va vous pousser à être motivé et à tenir sur la durée donc moi si vous êtes un skin fat je vous conseille de virer ce gras dans tous les cas il va pas vous servir vous avez pas besoin d'avoir un gros surplus calorique pour prendre de la masse musculaire et vous de toute façon vous avez cette ce petit excédent de gras qui vous donne de l'énergie pour vous entraîner et créer du muscle conclusion maintenant s'il y a juste quelques points à retenir de la vidéo pour vraiment tenir sur la durée et avoir vraiment ce bon physique que vous allez tenir en 2025 c'est ne commencez pas trop dur ne vous fixez pas des objectif irréalisable ne passez pas du tout au tout c'est-à-dire que si vous ne faites aucun entraînement et que vous mangez n'importe quoi ben ne vous mettez pas vous entraîer SEP fois par semaine avec une diète ultra restrictive n'allez pas à la salle sans savoir quoi faire organisez vos training organiser les temps et les jours où vous allez vous entraîner faites un vrai bilan objectif de votre physique pour pouvoir avoir une nutrition adaptée faites des choses simples et efficaces et pour terminer n'attendez pas que toutes les étoiles soient aligné pour commencer ce qui est très important c'est que ceux qui réussissent c'est ceux qui se lancent pas ceux qui attendent que ce soit parfait pour se lancer vous allez faire quelques erreurs c'est pas grave vous savez il y a un proverbe chinois qui dit le meilleur moment pour planter un arbre c'était il y a 20 ans et le deuxième meilleur moment c'est maintenant au final si tu te cherches encore des excuses c'est que peut-être tu as pas vraiment décidé de réussir on dit que il vaut mieux faire plein d'effort répété que de mettre un grand coup d'épée alors fais les choses maintenant et surtout surtout d'ici là cararde la pêche.
src/gradio/data/corpus_car_FULL_3.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ le nombre de séries à faire par exercice dans une séance de musculation ça reste quand même un point d'interrogation pour beaucoup de pratiquants est-ce qu'il faut faire 2 3 4 5 séries par exercice est-ce qu'il faut adapter le nombre de séries à l'objectif ou à l'exercice en question aujourd'hui je vais répondre à toutes ces interrogations bienvenue sur la chaîne fitmas je m'appelle nassim saili je suis le créateur des programmes fitmas des programmes qui sont faits pour vous quel que soit votre objectif que vous vouliez prendre du muscle perdre du gras constituer votre propre programme ou même avoir un accompagnement personnalisé avec l'un des coachs fitmas sur fitmas.fr vous retrouverez plein de programmes basés sur la science et qui sont évidemment conçus et testés par mes soins le lien du site fitmas.fr fr est dans la description et voyons ensemble quel est le nombre de séances parfait par exercice lorsqu'on cherche à déterminer le nombre de séries à effectuer par exercice il faut d'abord se poser la question suivante : combien de séries je vais effectuer par semaine et par groupe musculaire c'est un peu la base quand on va constituer un programme de musculation si on ne sait pas combien de séries on effectue par semaine et par groupe musculaire et bien c'est très difficile de pouvoir les répartir sur chacune de vos séances et sur chacun de vos exercices pourquoi est-ce qu'on prend ce référentiel là parce que toutes les études scientifiques sur le sujet ont été faites sur le nombre de séries hebdomadaires et par groupe musculaire petit exemple vous décidez pour les pectoraux de faire 20 séries dans la semaine et vous ne faites qu'une seule séance pectorau par semaine et bien ces 20 séries devront être réparti sur 3 4 5 exercices que vous allez effectuer dans votre séance pectorau et du coup si vous faites quatre exercices et que vous devez comptabiliser 20 séries vous en arrivez à 5 séries par exercice c'est juste un exemple sur le terrain le nombre de séries hebdomadaires doit être minutieusement choisi en fonction de votre objectif de votre expérience et des points forts points faibles que vous pourriez avoir mais lorsque vous avez compris que le plus important n'était pas nécessairement le nombre de séries par exercice mais plutôt le nombre total de séries par groupe musculaire et par semaine on a déjà fait un gros pas en avant le deuxième point important lorsque vous choisissez le nombre de séries à faire par exercice c'est la gestion de la fatigue j'ose espérer que dans votre programme de musculation vous avez sûrement déjà testé de faire trois séries quatre séries cin séries par exercice et je suis presque certain que vous avez remarqué que lorsque vous faites 4 5 voire même peut-être six séries et bien vos performances ont tendance à chuter assez rapidement c'est ça qu'on appelle la gestion de la fatigue vous n'êtes pas sans savoir que pour prendre du muscle il faut quand même soulever des charges de travail importantes soulever des charges de travail importantes vous permettra de provoquer une tension mécanique importante qui se traduira par de l'hypertrophie donc vos fibres musculaires qui vont grossir mais si au fur et à mesure de votre exercice vos performances ont tendance à chuter trop rapidement et bien forcément ça aura une répercussion sur l'intensité de travail et à terme sur votre progression tout ça veut dire que vous allez devoir faire un minimum d'expérimentation je vous recommande de tester plusieurs nombres de séries par exercice 3 4 5 voire même 6 et de vérifier à quel point vos performances descendent au fur et à mesure de ces séries si par exemple sur du développer couché vous êtes fixé des séries de 10 que vous arrivez à utiliser 100 kg sur la première série 100 kg sur la 2è série 90 kg sur la 3e série mais qu'à partir de la 4e série on descend à 70 60 la réduction est bien trop importante et dans ce cas-là il aurait fallu s'arrêter à la 3è série pourquoi je dis qu'il faut expérimenter parce que ça dépend de l'exercice du groupe musculaire de votre expérience à la salle de votre tolérance à la douleur donc malheureusement impossible de vous donner un chiffre qui fonctionnera pour tous les cas de figure mais ça tombe bien l'expérimentation vous permettra d'en apprendre beaucoup plus sur votre corps et pour compléter le point précédent en réalité il n'y a pas de minimum ni de maximum je me rappelle d'une anecdote super intéressante d'arnold schwarzenegger qui s'entraînait au gold gym pendant l'âge d'or du bodybuilding c'est-à-dire à peu près dans les années 70 il racontait à quel point lui-même expérimentait différents formats d'entraînement et différents nombres de séries par exercice allant jusqu'à faire des séances d'entraînement où il n'effectuaient qu'une seule série par exercice et donc multipliait le nombre d'exercices ou au contraire des séances d'entraînement où il n'y avait qu'un seul exercice et un nombre de séries assez délirant on part de 15 20 25 séries sur un seul exercice alors bien sûr ça c'est des pratiques extrêmes qui sont là juste pour expérimenter mais ils avaient pas d'autres choix à l'époque étant donné qu'il y avait pas autant d'études scientifiques qu'aujourd'hui mais ça nous permet de réaliser qu'il n'y a pas de minimum ni de maximum si vous arrivez à choisir un exercice et qu'il ne vous faut qu'une seule série pour atteindre l'intensité maximale la charge maximale atteindre l'échec musculaire sans aucun problème et que ça s'imbrique bien dans le reste de votre programmation d'entraînement c'est nickel et puis au contraire si vous êtes dans une phase d'entraînement où l'accumulation de volume va être importante il n'y a aucune contre-indication à faire 6 7 8 séries ou même du 10 x 10 par exemple qui a un format d'entraînement très populaire dans la musculation et d'ailleurs j'aurais même tendance à dire que varier le nombre de séries par exercice bah ça apporte un peu de variété un peu de nouveauté ça permet d'explorer différents types d'effort et à condition que vous soyez bien attentif à vos performances que vous notiez vos séances pour être sûr de suivre ce que vous avez fait bah je trouve que c'est une excellente idée d'explorer différents types d'entraînement pour voir comment votre corps répond je suis hyper fan du 6 x 6 du 8 x 8 ou du 10 x 10 que j'intègre très souvent dans mes propres séances d'entraînement et que j'intègre aussi dans les programmes fitmass en fonction de l'objectif ça peut permettre une stimulation musculaire hyper intéressante une dépense énergétique hyper intéressante ça apporte de la variété ça permet de voir où sont ses limites il y a que du positif derrière ça donc ne vous formalisez pas sur les séances classiques avec quatre séries de 10 explorer d'autres horizons et je suis sûr que ça rajoutera pas mal de choses à vos séances de musculation mais alors on peut se demander pourquoi la plupart des programmes de musculation on se retrouve avec un format très classique de quatre séries et parfois même de 10 à 12 répétitions en fait c'est un peu basé sur les études scientifiques qui ont été faites il y a un moment maintenant plusieurs décennies et qui recommandaient de faire entre 16 et 20 séries par semaine et par groupe musculaire et si on part du principe que chaque groupe musculaire était entraîné une fois par semaine avec quatre ou cinq exercices différents et bien on se retrouve 20 séries qui sont réparti en quatre ou cinq exercices donc environ quatre séries de travail dans une prochaine vidéo je pourrais vous expliquer pourquoi la plupart des programmes de musculation comportent des séries de 10 ou 12 répétitions mais pour le coup c'est un peu le même principe pour le nombre de séries il y a pas vraiment de justification et comme je l'ai précisé dans les points précédents rien ne nous empêche à faire un petit peu moins ou même beaucoup plus à condition de savoir pourquoi on fait ce choix dans mes propres programmes de musculation et ceux que je propose sur le site fitmas.fr je pars toujours du nombre de séries hebdomadaires par groupe musculaire et c'est avec cette variable qui est la plus importante de toutes que je vais faire ma petite répartition c'est exactement comme ça que je vous recommande de faire pour éviter de vous entraîner au hasard à l'instinct et de choisir le nombre de séries en fonction de votre humeur du jour mais sans vraie cohérence par rapport à votre programmation d'entraînement vous savez que s'entraîner à l'instinct c'est pas quelque chose que je recommande parce que c'est très très difficile de suivre ces performances et de les faire évoluer petit à petit et le nombre de séries par exercice ça reste une variable très importante qu'il faut traquer pour pouvoir la faire évoluer donc c'est jamais une mauvaise idée de faire quatre séries par exercice mais n'oubliez pas qu'on a beaucoup d'autres possibilités qui s'ouvrent à nous pour essayer de progresser le plus rapidement possible avec toutes ces informations lorsque vous commencerez l'un des programmes fitmas pas être surpris si vous voyez certains exercices où il n'y a que deux séries et d'autres exercices on peut monter très haut 8 10 séries de travail que ce soit d'ailleurs dans vrt mon programme spécial prise de muscle ou dans superet mon programme spécial perte de gras j'utilise plein de fourchettes de répétition différentes plein de formats différents et des nombres de séries par exercice qui peuvent varier en fonction de l'objectif du cycle en question et de ce qu'on aimerait provoquer sur le corps et d'ailleurs tout ça je l'explique bien plus en détail dans fitmas builder qui vous permet de construire votre propre programme en fonction de vos spécificités votre emploi du temps vos points forts vos points faibles c'est hyper complet donc si jamais vous voulez jeter un œil vous cliquez sur le premier lien dans la description je suis sûr que sur fitmas.fr vous trouverez le programme qui vous correspond que vous soyez un homme ou une femme quel que soit votre objectif quelle que soit votre fréquence d'entraînement donc allez y faire un tour et bon courage lorsque vous commencerez votre programme.
src/gradio/data/dataset_exercices_fusion_20251127_2004.json ADDED
The diff for this file is too large to render. See raw diff
 
src/gradio/data/goal_map.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ "Athletics",
3
+ "Bodybuilding",
4
+ "Bodyweight Fitness",
5
+ "Muscle & Sculpting",
6
+ "Olympic Weightlifting",
7
+ "Powerbuilding",
8
+ "Powerlifting"
9
+ ]
src/gradio/data/program_summary.csv ADDED
The diff for this file is too large to render. See raw diff
 
src/gradio/generators/execution_generator.py ADDED
@@ -0,0 +1,345 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ from functools import lru_cache
4
+ from typing import Mapping, Union, List
5
+
6
+ import re
7
+ import pandas as pd
8
+ import torch
9
+ from transformers import AutoTokenizer, AutoModelForCausalLM
10
+
11
+ # ---------------------------------------------------------------------
12
+ # Paths + device
13
+ # ---------------------------------------------------------------------
14
+
15
+ PROJECT_ROOT = Path(__file__).resolve().parents[2]
16
+ MODEL_DIR = PROJECT_ROOT / "models" / "v1"
17
+
18
+ # Dossier de ton modèle finetuné d'exécution
19
+ # EXEC_MODEL_DIR = MODEL_DIR / "transformer_execution_generator_v3"
20
+ # Chargement du modèle HF (tokenizer + modèle)
21
+ MODEL_REPO = "AIppyDev/transformer_execution_generator_v3"
22
+ MODEL_SUBFOLDER = "transformer_execution_generator_v3" # le nom du dossier dans le repo
23
+ REPORT_PATH = MODEL_DIR / "execution_generator_model_report.json"
24
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
25
+
26
+ # Nombre max de tokens générés (équivalent MAX_LENGTH du notebook)
27
+ EXEC_MAX_NEW_TOKENS = 180
28
+
29
+ # ---------------------------------------------------------------------
30
+ # Construction du prompt pour un programme
31
+ # ---------------------------------------------------------------------
32
+
33
+ def build_execution_prompt(row: Union[pd.Series, Mapping, None]) -> str:
34
+ """
35
+ Construit le prompt d'exécution à partir d'une ligne de programme.
36
+
37
+ Format :
38
+ The {exercise_name} exercise for "{target_muscles}" with "{equipment}" on "{difficulty}" level:
39
+ """
40
+ if row is None:
41
+ return ""
42
+
43
+ def get_val(key: str, default: str = "") -> str:
44
+ if isinstance(row, pd.Series):
45
+ val = row.get(key, default)
46
+ else:
47
+ val = row.get(key, default) if hasattr(row, "get") else default
48
+
49
+ if pd.isna(val):
50
+ return default
51
+ return str(val).strip()
52
+
53
+ name = get_val("exercise_name", "this exercise")
54
+ muscle = get_val("target_muscles", "the target muscles")
55
+ equipment = get_val("equipment", "bodyweight")
56
+ difficulty = get_val("difficulty", "General")
57
+
58
+ prompt = (
59
+ f'The {name} exercise for '
60
+ f'"{muscle}" with '
61
+ f'"{equipment}" on '
62
+ f'"{difficulty}" level:'
63
+ )
64
+ return prompt
65
+
66
+
67
+ # ---------------------------------------------------------------------
68
+ # Utils de post-traitement
69
+ # ---------------------------------------------------------------------
70
+
71
+ def strip_prompt_from_generations(prompts: List[str], generated_outputs: List[str]) -> List[str]:
72
+ """
73
+ Supprime le prompt au début de chaque génération si le modèle l'a recopié.
74
+ """
75
+ cleaned = []
76
+
77
+ for prompt, gen in zip(prompts, generated_outputs):
78
+ gen_strip = gen.strip()
79
+
80
+ if gen_strip.startswith(prompt):
81
+ cleaned.append(gen_strip[len(prompt):].strip())
82
+ else:
83
+ cleaned.append(gen_strip)
84
+
85
+ return cleaned
86
+
87
+
88
+ def trim_to_last_full_sentence(text: str) -> str:
89
+ """
90
+ Coupe le texte à la dernière phrase complète.
91
+ On garde tout jusqu'au dernier '.', '!' ou '?'.
92
+ Si aucun n'est trouvé, on renvoie le texte brut stripé.
93
+ """
94
+ text = text.strip()
95
+
96
+ last_dot = text.rfind(".")
97
+ last_excl = text.rfind("!")
98
+ last_q = text.rfind("?")
99
+
100
+ last_punct = max(last_dot, last_excl, last_q)
101
+
102
+ if last_punct != -1:
103
+ return text[: last_punct + 1].strip()
104
+ return text
105
+
106
+
107
+ def keep_first_n_sentences(text: str, n: int = 2) -> str:
108
+ """
109
+ Garde seulement les n premières phrases (séparation grossière sur . ! ?).
110
+ """
111
+ sentences = re.split(r'([.!?])', text)
112
+ chunks = []
113
+ count = 0
114
+
115
+ for i in range(0, len(sentences) - 1, 2):
116
+ sentence = sentences[i].strip()
117
+ punct = sentences[i + 1].strip()
118
+ if sentence:
119
+ chunks.append(sentence + punct)
120
+ count += 1
121
+ if count >= n:
122
+ break
123
+
124
+ return " ".join(chunks).strip() if chunks else text.strip()
125
+
126
+
127
+ def dedupe_muscle_list(text: str) -> str:
128
+ """
129
+ Détecte les listes du type 'glutes, hamstrings, and hamstrings'
130
+ dans la phrase, les déduplique et les reformate proprement.
131
+ """
132
+
133
+ # Regex qui capture une liste séparée par virgules ou 'and'
134
+ # Exemple capturé : "glutes, hamstrings, and hamstrings"
135
+ pattern = r"([A-Za-z ]+(?:,\s*[A-Za-z ]+)*(?:\s+and\s+[A-Za-z ]+)?)"
136
+
137
+ def process_match(match):
138
+ segment = match.group(0)
139
+
140
+ # Split sur virgules et 'and'
141
+ parts = re.split(r",|\band\b", segment)
142
+ parts = [p.strip() for p in parts if p.strip()]
143
+
144
+ # Déduplique tout en gardant l’ordre
145
+ seen = set()
146
+ unique = []
147
+ for p in parts:
148
+ if p.lower() not in seen:
149
+ seen.add(p.lower())
150
+ unique.append(p)
151
+
152
+ # Reconstruction naturelle
153
+ if len(unique) == 1:
154
+ return unique[0]
155
+
156
+ if len(unique) == 2:
157
+ return f"{unique[0]} and {unique[1]}"
158
+
159
+ return ", ".join(unique[:-1]) + f", and {unique[-1]}"
160
+
161
+ # Applique la fonction sur toutes les occurrences
162
+ cleaned = re.sub(pattern, process_match, text)
163
+ return cleaned
164
+
165
+
166
+ def clean_execution_text(text: str) -> str:
167
+ """
168
+ Nettoyage final : espaces, 'reps', etc.
169
+ """
170
+ if not isinstance(text, str):
171
+ return text
172
+
173
+ cleaned = text
174
+
175
+ # 1) Ajouter un espace après un point si collé à une majuscule
176
+ cleaned = re.sub(r'(\.)([A-Z])', r'\1 \2', cleaned)
177
+
178
+ # 2) Ajouter un espace après "such as" si collé à un chiffre
179
+ cleaned = re.sub(r"(such as)(\d)", r"\1 \2", cleaned)
180
+
181
+ # 3) Ajouter un espace avant "reps" si collé au chiffre
182
+ cleaned = re.sub(r"(\d)(reps)", r"\1 reps", cleaned)
183
+
184
+ # 4) Normaliser les doubles espaces
185
+ cleaned = re.sub(r"\s{2,}", " ", cleaned).strip()
186
+
187
+ return cleaned
188
+
189
+
190
+ # ---------------------------------------------------------------------
191
+ # Chargement du modèle HF (tokenizer + modèle)
192
+ # ---------------------------------------------------------------------
193
+
194
+ @lru_cache()
195
+ def _load_exec_model():
196
+ """
197
+ Charge une seule fois tokenizer + modèle pour l'execution generator.
198
+ Utilise un cache pour éviter les rechargements coûteux.
199
+ """
200
+
201
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_REPO,
202
+ subfolder=MODEL_SUBFOLDER,)
203
+ model = AutoModelForCausalLM.from_pretrained(
204
+ MODEL_REPO,
205
+ subfolder=MODEL_SUBFOLDER,
206
+ torch_dtype=torch.float32, # CPU friendly
207
+ )
208
+ model.to(DEVICE)
209
+ model.eval()
210
+
211
+ return tokenizer, model
212
+
213
+
214
+ # ---------------------------------------------------------------------
215
+ # Génération de texte pour l'exécution (1 prompt, pipeline complet)
216
+ # ---------------------------------------------------------------------
217
+
218
+ def generate_execution_text(
219
+ prompt: str,
220
+ max_new_tokens: int = EXEC_MAX_NEW_TOKENS,
221
+ temperature: float = 0.8,
222
+ top_k: int = 250,
223
+ top_p: float = 0.92,
224
+ ) -> str:
225
+ """
226
+ Génère une description d'exécution à partir d'un prompt unique,
227
+ avec le même pipeline que dans le notebook d'entraînement.
228
+ """
229
+ prompt = (prompt or "").strip()
230
+ if not prompt:
231
+ return "No prompt available to generate execution."
232
+
233
+ tokenizer, model = _load_exec_model()
234
+
235
+ sample_prompts = [prompt]
236
+
237
+ encodings = tokenizer(
238
+ sample_prompts,
239
+ return_tensors="pt",
240
+ padding=True,
241
+ truncation=True,
242
+ )
243
+ encodings = {k: v.to(DEVICE) for k, v in encodings.items()}
244
+
245
+ # 1. Génération
246
+ outputs = model.generate(
247
+ **encodings,
248
+ max_new_tokens=max_new_tokens, # nombre maximum de tokens générés
249
+ min_new_tokens=50, # longueur minimale
250
+ do_sample=True, # sampling activé (obligatoire pour top-k / top-p)
251
+ temperature=temperature, # légère "chauffe" pour plus de variété
252
+ top_k=top_k, # top-K sampling large
253
+ top_p=top_p, # nucleus sampling
254
+ no_repeat_ngram_size=3, # évite les répétitions de 3-grams
255
+ num_beams=5, # beam search pour améliorer la cohérence
256
+ num_return_sequences=1, # une seule génération par prompt
257
+ pad_token_id=tokenizer.eos_token_id,
258
+ early_stopping=True, # arrêt anticipé si EOS atteint partout
259
+ )
260
+
261
+ generated_outputs = tokenizer.batch_decode(
262
+ outputs,
263
+ skip_special_tokens=True,
264
+ )
265
+
266
+ # 2. On enlève le prompt au début de chaque génération
267
+ cleaned_generations = strip_prompt_from_generations(
268
+ prompts=sample_prompts,
269
+ generated_outputs=generated_outputs,
270
+ )
271
+
272
+ # 3. Post-traitement élément par élément
273
+ trimmed = [trim_to_last_full_sentence(txt) for txt in cleaned_generations]
274
+ limited = [keep_first_n_sentences(t, n=2) for t in trimmed]
275
+ deduped = [dedupe_muscle_list(t) for t in limited]
276
+ final = [clean_execution_text(t) for t in deduped]
277
+
278
+ return final[0] if final else ""
279
+
280
+
281
+ def get_dl_execution_model_report_components():
282
+ """
283
+ Retourne 4 DataFrames Gradio-ready :
284
+ - Summary
285
+ - Model
286
+ - Training
287
+ - Metrics
288
+
289
+ Si pas de rapport → retourne les DF vides.
290
+ """
291
+
292
+ report_path = REPORT_PATH
293
+ if not report_path or not report_path.exists():
294
+ return _empty_dl_dfs()
295
+
296
+ try:
297
+ data = json.loads(report_path.read_text(encoding="utf-8"))
298
+ except Exception as e:
299
+ print(f"[DL REPORT] Error while reading {report_path}: {e}")
300
+ return _empty_dl_dfs()
301
+
302
+ # ===== Summary =====
303
+ dataset = data.get("dataset", {})
304
+
305
+ summary_rows = [
306
+ ("created_at", data.get("created_at", "")),
307
+ ("task", data.get("task", "")),
308
+ ("target", data.get("target", "")),
309
+ ("framework", data.get("framework", "")),
310
+ ("dataset.file", dataset.get("file", "")),
311
+ ("dataset.size_bytes", dataset.get("size_bytes", "")),
312
+ ("dataset.tokens", dataset.get("tokens", "")),
313
+ ]
314
+
315
+ df_summary = pd.DataFrame(summary_rows, columns=["Key", "Value"])
316
+
317
+ # ===== Model config =====
318
+ model_cfg = data.get("model", {}) or {}
319
+ df_model = pd.DataFrame(
320
+ [(k, v) for k, v in model_cfg.items()],
321
+ columns=["Key", "Value"],
322
+ )
323
+
324
+ # ===== Training =====
325
+ training_cfg = data.get("training", {}) or {}
326
+ df_training = pd.DataFrame(
327
+ [(k, v) for k, v in training_cfg.items()],
328
+ columns=["Key", "Value"],
329
+ )
330
+
331
+ # ===== Metrics =====
332
+ metrics_cfg = data.get("metrics", {}) or {}
333
+ df_metrics = pd.DataFrame(
334
+ [(k, v) for k, v in metrics_cfg.items()],
335
+ columns=["Metric", "Value"],
336
+ )
337
+
338
+ return df_summary, df_model, df_training, df_metrics
339
+
340
+ def _empty_dl_dfs():
341
+ df_summary = pd.DataFrame({"Key": [], "Value": []})
342
+ df_model = pd.DataFrame({"Key": [], "Value": []})
343
+ df_training = pd.DataFrame({"Key": [], "Value": []})
344
+ df_metrics = pd.DataFrame({"Metric": [], "Value": []})
345
+ return df_summary, df_model, df_training, df_metrics
src/gradio/generators/gpt2_distillation_text_generator.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import textwrap
2
+ import torch
3
+ import re
4
+
5
+ class GPT2_DistilledTextGenerator:
6
+ """
7
+ Wrapper pour le modèle GPT-2 distillé de TrAIn.me.
8
+ Gère :
9
+ - génération auto-régressive
10
+ - temperature / top_p
11
+ - max_new_tokens
12
+ """
13
+
14
+ def __init__(self, model, tokenizer, max_new_tokens: int = 256):
15
+ self.model = model
16
+ self.tokenizer = tokenizer
17
+ self.max_new_tokens = max_new_tokens
18
+
19
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
20
+ self.model.to(self.device)
21
+ self.model.eval()
22
+
23
+ # ------------------------------------------------------------------
24
+ # Génération simple
25
+ # ------------------------------------------------------------------
26
+ def generate_text(
27
+ self,
28
+ prompt: str,
29
+ temperature: float = 0.8,
30
+ top_p: float = 0.9,
31
+ strip_prompt: bool = True,
32
+ ) -> str:
33
+
34
+ prompt = prompt.strip()
35
+ if not prompt:
36
+ return "Please enter a prompt before generating."
37
+
38
+ inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device)
39
+
40
+ with torch.no_grad():
41
+ output_ids = self.model.generate(
42
+ **inputs,
43
+ max_new_tokens=self.max_new_tokens,
44
+ do_sample=True,
45
+ temperature=temperature,
46
+ top_p=top_p,
47
+ pad_token_id=self.tokenizer.eos_token_id,
48
+ eos_token_id=self.tokenizer.eos_token_id,
49
+ )
50
+
51
+ text = self.tokenizer.decode(output_ids[0], skip_special_tokens=True)
52
+
53
+ if strip_prompt and text.startswith(prompt):
54
+ text = text[len(prompt):].lstrip()
55
+
56
+ return text
57
+
58
+ # ------------------------------------------------------------------
59
+ # Ancienne fonction interactive (simple)
60
+ # ------------------------------------------------------------------
61
+ def generer_exercice_interactif(
62
+ self,
63
+ workout_type: str = "strength",
64
+ debut: str = "",
65
+ num_samples: int = 3,
66
+ max_length: int = 120,
67
+ temperature: float = 1.0,
68
+ ):
69
+ device = torch.device("cpu")
70
+
71
+ if debut:
72
+ prompt = f"Workout [{workout_type}]: {debut}"
73
+ else:
74
+ prompt = f"Workout [{workout_type}]:"
75
+
76
+ print(f"\nGénération de {num_samples} exemples ({workout_type.upper()})")
77
+ print(f"Prompt : '{prompt}'\n")
78
+ print("=" * 80)
79
+
80
+ self.model.to(device)
81
+ self.model.eval()
82
+
83
+ for i in range(num_samples):
84
+ inputs = self.tokenizer(prompt, return_tensors="pt").to(device)
85
+
86
+ with torch.no_grad():
87
+ output_ids = self.model.generate(
88
+ **inputs,
89
+ max_length=max_length,
90
+ do_sample=True,
91
+ temperature=temperature,
92
+ top_k=50,
93
+ top_p=0.95,
94
+ eos_token_id=self.tokenizer.eos_token_id,
95
+ pad_token_id=self.tokenizer.eos_token_id,
96
+ )
97
+
98
+ generated_text = self.tokenizer.decode(
99
+ output_ids[0],
100
+ skip_special_tokens=True
101
+ )
102
+
103
+ return textwrap.fill(
104
+ generated_text,
105
+ width=75,
106
+ initial_indent=" ",
107
+ subsequent_indent=" ",
108
+ )
109
+
110
+
111
+ # ------------------------------------------------------------------
112
+ # Filtre amélioré
113
+ # ------------------------------------------------------------------
114
+ def generate_with_filter(
115
+ self,
116
+ model,
117
+ tokenizer,
118
+ prompt: str,
119
+ goal: str,
120
+ n_candidates: int = 3,
121
+ max_new_tokens: int = 160,
122
+ temperature: float = 0.7,
123
+ top_k: int = 40,
124
+ top_p: float = 0.9,
125
+ max_attempts: int = 3,
126
+ ):
127
+ device = torch.device("cpu")
128
+ model.to(device)
129
+ model.eval()
130
+
131
+ self.model.to(device)
132
+ self.model.eval()
133
+
134
+ for i in range(1):
135
+ inputs = self.tokenizer(prompt, return_tensors="pt").to(device)
136
+
137
+ with torch.no_grad():
138
+ output_ids = self.model.generate(
139
+ **inputs,
140
+ max_length=160,
141
+ do_sample=True,
142
+ temperature=temperature,
143
+ top_k=50,
144
+ top_p=0.95,
145
+ eos_token_id=self.tokenizer.eos_token_id,
146
+ pad_token_id=self.tokenizer.eos_token_id,
147
+ )
148
+
149
+ generated_text = self.tokenizer.decode(
150
+ output_ids[0],
151
+ skip_special_tokens=True
152
+ )
153
+
154
+ return textwrap.fill(
155
+ generated_text,
156
+ width=75,
157
+ initial_indent=" ",
158
+ subsequent_indent=" ",
159
+ )
160
+
161
+ # ------------------------------------------------------------------
162
+ # Version V2 pour l'IHM
163
+ # ------------------------------------------------------------------
164
+
165
+
166
+
167
+ def generer_exercice_interactif_V2(
168
+ self,
169
+ level: str,
170
+ goal: str,
171
+ num_samples: int = 3,
172
+ max_length: int = 150,
173
+ temperature: float = 1.0,
174
+ candidates_per_sample: int = 3,
175
+ ):
176
+
177
+ prompt = f"{level} level ({goal})\n\n"
178
+
179
+ texts = []
180
+
181
+ for i in range(num_samples):
182
+ # ⬇️ ICI : appel via self, plus via la classe
183
+ generated_text = self.generate_with_filter(
184
+ model=self.model,
185
+ tokenizer=self.tokenizer,
186
+ prompt=prompt,
187
+ goal=goal,
188
+ n_candidates=candidates_per_sample,
189
+ max_new_tokens=160,
190
+ temperature=0.7,
191
+ top_k=40,
192
+ top_p=0.9,
193
+ )
194
+
195
+
196
+
197
+ # Suppression des artefacts de liste Python
198
+ cleaned = generated_text.replace("['", "")
199
+ cleaned = cleaned.replace("']", "")
200
+ cleaned = cleaned.replace('", "', ' ') # parfois utilisé dans les listes
201
+
202
+ # Supprimer les retours à la ligne trop nombreux
203
+ cleaned = cleaned.replace("\n", " ")
204
+
205
+ # Retirer le prompt si le modèle l'a recopié
206
+ prompt_strip = prompt.strip()
207
+ if prompt_strip and cleaned.startswith(prompt_strip):
208
+ cleaned = cleaned[len(prompt_strip):].lstrip()
209
+
210
+ # Suppression des doubles espaces
211
+ cleaned = re.sub(r"\s{2,}", " ", cleaned)
212
+
213
+ # Trim final
214
+ cleaned = cleaned.strip()
215
+
216
+
217
+ texts.append(
218
+ textwrap.fill(
219
+ cleaned,
220
+ width=75,
221
+ initial_indent=" ",
222
+ subsequent_indent=" ",
223
+ )
224
+ )
225
+
226
+ return texts
227
+
src/gradio/generators/gpt2_fine_tuning_text_generator.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ GPT2_FINE_TUNING_GENERATOR: "GPT2_FineTuningTextGenerator | None" = None
4
+
5
+
6
+ class GPT2_FineTuningTextGenerator:
7
+ """
8
+ Wrapper simple autour du modèle GPT-2 fine-tuné de TrAIn.me.
9
+ Gère :
10
+ - la génération autoregressive
11
+ - temperature / top_p
12
+ - max_new_tokens
13
+ """
14
+
15
+ def __init__(self, model, tokenizer, max_new_tokens: int = 256):
16
+ self.model = model
17
+ self.tokenizer = tokenizer
18
+ self.max_new_tokens = max_new_tokens
19
+
20
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
21
+ self.model.to(self.device)
22
+ self.model.eval()
23
+
24
+ def generate_text(
25
+ self,
26
+ prompt: str,
27
+ temperature: float = 0.9,
28
+ top_p: float = 0.95,
29
+ strip_prompt: bool = True,
30
+ ) -> str:
31
+ """Génère du texte à partir du prompt."""
32
+ prompt = prompt.strip()
33
+ if not prompt:
34
+ return "Please enter a prompt before generating."
35
+
36
+ inputs = self.tokenizer(prompt, return_tensors="pt").to(self.device)
37
+
38
+ with torch.no_grad():
39
+ output_ids = self.model.generate(
40
+ **inputs,
41
+ max_new_tokens=self.max_new_tokens,
42
+ do_sample=True,
43
+ temperature=temperature,
44
+ top_p=top_p,
45
+ pad_token_id=self.tokenizer.eos_token_id,
46
+ eos_token_id=self.tokenizer.eos_token_id,
47
+ )
48
+
49
+ text = self.tokenizer.decode(output_ids[0], skip_special_tokens=True)
50
+
51
+ if strip_prompt and text.startswith(prompt):
52
+ text = text[len(prompt):].lstrip()
53
+
54
+ return text
src/gradio/generators/lstm_text_generator.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from pathlib import Path
3
+ from collections import OrderedDict
4
+
5
+ import numpy as np
6
+ import torch
7
+ import torch.nn as nn
8
+ import pandas as pd # <-- NEW
9
+
10
+ from tensorflow.keras.preprocessing.text import Tokenizer
11
+ from tensorflow.keras.preprocessing.sequence import pad_sequences
12
+
13
+
14
+ class LSTMTextGenerator:
15
+ """
16
+ Gestionnaire dédié pour le LSTM :
17
+ - reçoit un modèle déjà chargé (PyTorch ou Keras) depuis on_model_change
18
+ - OU un state_dict PyTorch (OrderedDict) → reconstruit alors un nn.Module
19
+ - charge + nettoie le corpus texte
20
+ - recrée le tokenizer (identique au notebook LSTM v3)
21
+ - génère du texte à partir d'un seed.
22
+ """
23
+
24
+ # Racine du projet + chemin vers le corpus CSV
25
+ PROJECT_ROOT = Path(__file__).resolve().parents[2]
26
+ CORPUS_CSV_PATH = PROJECT_ROOT / "gradio" / "data" / "program_summary.csv"
27
+
28
+ # Colonnes textuelles utilisées dans le notebook LSTM v3
29
+ TEXT_COLUMNS = [
30
+ "program_title",
31
+ "description",
32
+ "goal",
33
+ "target_muscles",
34
+ "equipment",
35
+ "instructions",
36
+ ]
37
+
38
+ # Longueur de séquence par défaut (cf. LSTM_model_report.json)
39
+ DEFAULT_SEQUENCE_LENGTH = 15
40
+
41
+ # Singleton interne
42
+ _instance: "LSTMTextGenerator | None" = None
43
+
44
+ # ----------------------------------------------------------------------
45
+ # Constructeur
46
+ # ----------------------------------------------------------------------
47
+ def __init__(self, model, tokenizer, max_length: int | None = None):
48
+
49
+ # Si on reçoit un state_dict PyTorch → reconstruire un nn.Module
50
+ if isinstance(model, OrderedDict):
51
+ model = self._build_torch_model_from_state_dict(model)
52
+
53
+ self.model = model
54
+ self.tokenizer = tokenizer
55
+ self.max_length = max_length or self.DEFAULT_SEQUENCE_LENGTH
56
+
57
+ # Backend PyTorch ou Keras
58
+ self._is_torch = isinstance(self.model, torch.nn.Module)
59
+ if self._is_torch:
60
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
61
+ self.model.to(self.device)
62
+ self.model.eval()
63
+ else:
64
+ self.device = None
65
+
66
+ # ----------------------------------------------------------------------
67
+ # Reconstruction d'un modèle PyTorch à partir d'un state_dict
68
+ # ----------------------------------------------------------------------
69
+ @staticmethod
70
+ def _build_torch_model_from_state_dict(state: OrderedDict) -> torch.nn.Module:
71
+ """
72
+ Reconstruit dynamiquement un LSTM language model standard à partir d'un state_dict
73
+ de la forme :
74
+
75
+ embedding.weight -> (vocab_size, embedding_dim)
76
+ lstm.weight_ih_l0 -> (4*hidden_dim, embedding_dim)
77
+ fc.weight -> (vocab_size, hidden_dim)
78
+ """
79
+
80
+ emb_weight = state.get("embedding.weight", None)
81
+ fc_weight = state.get("fc.weight", None)
82
+ lstm_weight_ih_l0 = state.get("lstm.weight_ih_l0", None)
83
+
84
+ if emb_weight is None or fc_weight is None or lstm_weight_ih_l0 is None:
85
+ raise TypeError(
86
+ "Impossible de reconstruire le LSTM PyTorch : "
87
+ "les clés attendues 'embedding.weight', 'lstm.weight_ih_l0', "
88
+ "'fc.weight' sont absentes du state_dict. "
89
+ )
90
+
91
+ vocab_size, embedding_dim = emb_weight.shape
92
+ out_features, hidden_dim = fc_weight.shape
93
+
94
+ if out_features != vocab_size:
95
+ print(
96
+ f"[LSTMTextGenerator] Avertissement : fc.weight shape={fc_weight.shape} "
97
+ f"→ out_features != vocab_size ({out_features} != {vocab_size})."
98
+ )
99
+
100
+ num_layers = 1
101
+ for n in range(1, 10):
102
+ if f"lstm.weight_ih_l{n}" in state:
103
+ num_layers = n + 1
104
+ else:
105
+ break
106
+
107
+ class TorchLSTMLanguageModel(nn.Module):
108
+ def __init__(self, vocab_size, embedding_dim, hidden_dim, num_layers):
109
+ super().__init__()
110
+ self.embedding = nn.Embedding(vocab_size, embedding_dim)
111
+ self.lstm = nn.LSTM(
112
+ input_size=embedding_dim,
113
+ hidden_size=hidden_dim,
114
+ num_layers=num_layers,
115
+ batch_first=True,
116
+ )
117
+ self.fc = nn.Linear(hidden_dim, vocab_size)
118
+
119
+ def forward(self, x):
120
+ emb = self.embedding(x) # (batch, seq_len, embed_dim)
121
+ out, _ = self.lstm(emb) # (batch, seq_len, hidden_dim)
122
+ logits = self.fc(out) # (batch, seq_len, vocab_size)
123
+ return logits
124
+
125
+ model = TorchLSTMLanguageModel(
126
+ vocab_size=vocab_size,
127
+ embedding_dim=embedding_dim,
128
+ hidden_dim=hidden_dim,
129
+ num_layers=num_layers,
130
+ )
131
+ model.load_state_dict(state)
132
+ return model
133
+
134
+ # ----------------------------------------------------------------------
135
+ # Méthodes de classe : singleton
136
+ # ----------------------------------------------------------------------
137
+ @classmethod
138
+ def get_instance(cls, model) -> "LSTMTextGenerator":
139
+ """
140
+ Retourne une unique instance de LSTMTextGenerator, initialisée à partir :
141
+ - d'un modèle LSTM déjà chargé (Keras ou PyTorch)
142
+ - ou d'un state_dict PyTorch (OrderedDict)
143
+ - du corpus texte dans data/programs/program_summary.csv
144
+ """
145
+ if cls._instance is not None:
146
+ return cls._instance
147
+
148
+ # Charger + nettoyer le corpus (version CSV anglaise)
149
+ full_text_clean = cls._load_full_clean_corpus()
150
+
151
+ # Recréer le tokenizer EXACT comme dans le notebook
152
+ tokenizer = cls.build_tokenizer_from_corpus(full_text_clean)
153
+
154
+ max_length = cls.DEFAULT_SEQUENCE_LENGTH
155
+ if hasattr(model, "input_shape") and getattr(model, "input_shape") is not None:
156
+ try:
157
+ max_length = int(model.input_shape[1]) + 1
158
+ except Exception:
159
+ pass
160
+
161
+ cls._instance = cls(
162
+ model=model,
163
+ tokenizer=tokenizer,
164
+ max_length=max_length,
165
+ )
166
+ return cls._instance
167
+
168
+ # ----------------------------------------------------------------------
169
+ # Helpers internes : corpus + nettoyage + tokenizer
170
+ # ----------------------------------------------------------------------
171
+ @classmethod
172
+ def _load_full_clean_corpus(cls) -> str:
173
+ """
174
+ Version LSTM v3 :
175
+ - charge `program_summary.csv`
176
+ - concatène plusieurs colonnes textuelles
177
+ - applique clean_text(...)
178
+ """
179
+ csv_path = cls.CORPUS_CSV_PATH
180
+ if not csv_path.exists():
181
+ raise FileNotFoundError(f"Corpus CSV not found: {csv_path}")
182
+
183
+ df = pd.read_csv(csv_path)
184
+
185
+ # On garde seulement les colonnes réellement présentes
186
+ text_cols = [c for c in cls.TEXT_COLUMNS if c in df.columns]
187
+ if not text_cols:
188
+ raise ValueError(
189
+ f"Aucune des colonnes textuelles attendues {cls.TEXT_COLUMNS} "
190
+ f"n'a été trouvée dans {csv_path.name}."
191
+ )
192
+
193
+ df[text_cols] = df[text_cols].fillna("")
194
+
195
+ # Concaténation ligne par ligne (comme dans le notebook)
196
+ lines = []
197
+ for _, row in df[text_cols].iterrows():
198
+ line = " ".join(str(row[c]) for c in text_cols).strip()
199
+ if line:
200
+ lines.append(line)
201
+
202
+ full_text = "\n".join(lines)
203
+ return cls.clean_text(full_text)
204
+
205
+ @staticmethod
206
+ def clean_text(texte: str) -> str:
207
+ """Nettoie et normalise le texte (version notebook)."""
208
+ texte = texte.lower()
209
+ texte = texte.replace("'", "")
210
+ texte = re.sub(r"([.,!?])", r" \1 ", texte)
211
+ texte = re.sub(r"\s+", " ", texte)
212
+ return texte.strip()
213
+
214
+ @staticmethod
215
+ def build_tokenizer_from_corpus(full_text_clean: str) -> Tokenizer:
216
+ tok = Tokenizer(filters="", lower=False, oov_token="<UNK>")
217
+ tok.fit_on_texts([full_text_clean])
218
+ return tok
219
+
220
+ # ----------------------------------------------------------------------
221
+ # Prédiction : unification Keras / PyTorch
222
+ # ----------------------------------------------------------------------
223
+ def _predict_proba(self, token_list: np.ndarray) -> np.ndarray:
224
+ if self._is_torch:
225
+ x = torch.from_numpy(token_list).long().to(self.device)
226
+ with torch.no_grad():
227
+ logits = self.model(x)
228
+ if logits.dim() == 3:
229
+ logits = logits[:, -1, :]
230
+ logits = logits[0]
231
+ probs = torch.softmax(logits, dim=-1).cpu().numpy()
232
+ return probs
233
+ else:
234
+ preds = self.model.predict(token_list, verbose=0)
235
+ return preds[0]
236
+
237
+ # ----------------------------------------------------------------------
238
+ # Génération auto-régressive
239
+ # ----------------------------------------------------------------------
240
+ def generate_text(
241
+ self,
242
+ seed_text: str,
243
+ num_words: int = 40,
244
+ temperature: float = 0.8,
245
+ seed: int | None = None,
246
+ ) -> str:
247
+ rng = np.random.default_rng(seed) if seed is not None else np.random
248
+ generated_text = seed_text
249
+
250
+ for _ in range(num_words):
251
+ token_list = self.tokenizer.texts_to_sequences([generated_text])[0]
252
+ token_list = pad_sequences(
253
+ [token_list],
254
+ maxlen=self.max_length - 1,
255
+ padding="pre",
256
+ )
257
+
258
+ predictions = self._predict_proba(token_list)
259
+ predictions = np.log(predictions + 1e-7) / temperature
260
+ predictions = np.exp(predictions) / np.sum(np.exp(predictions))
261
+
262
+ predicted_id = rng.choice(len(predictions), p=predictions)
263
+
264
+ predicted_word = ""
265
+ for word, index in self.tokenizer.word_index.items():
266
+ if index == predicted_id:
267
+ predicted_word = word
268
+ break
269
+
270
+ if predicted_word:
271
+ generated_text += " " + predicted_word
272
+
273
+ return generated_text
src/gradio/generators/transformer_text_generator.py ADDED
@@ -0,0 +1,552 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ from pathlib import Path
3
+ from collections import OrderedDict
4
+ from typing import Optional, Union, Mapping
5
+
6
+ import numpy as np
7
+ import torch
8
+ import torch.nn as nn
9
+ from tensorflow.keras.preprocessing.text import Tokenizer
10
+ from tensorflow.keras.preprocessing.sequence import pad_sequences
11
+
12
+
13
+ # ======================================================================
14
+ # BLOCS PYTORCH : doivent matcher ceux du notebook transformer_trainme_v2
15
+ # ======================================================================
16
+
17
+
18
+ class PositionalEmbedding(nn.Module):
19
+ """
20
+ Embedding de tokens + encodage positionnel (version légère).
21
+
22
+ Dans le notebook v2, la seule partie entraînable côté embedding est
23
+ `token_emb : Embedding(vocab_size, embed_dim)`.
24
+ La partie positionnelle peut rester non-paramétrique.
25
+ """
26
+
27
+ def __init__(self, vocab_size: int, embed_dim: int, max_length: int):
28
+ super().__init__()
29
+ self.token_emb = nn.Embedding(vocab_size, embed_dim)
30
+ self.max_length = max_length
31
+
32
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
33
+ """
34
+ x : (batch, seq_len) → retourne (batch, seq_len, embed_dim)
35
+ On se contente ici d'un token embedding + encodage positionnel implicite
36
+ (comme dans le notebook, aucun poids supplémentaire n'était enregistré
37
+ dans le state_dict).
38
+ """
39
+ # (batch, seq_len, embed_dim)
40
+ token_embeddings = self.token_emb(x)
41
+ return token_embeddings
42
+
43
+
44
+ class MultiHeadSelfAttention(nn.Module):
45
+ """
46
+ Multi-Head Self-Attention maison, comme dans le notebook v2.
47
+
48
+ - query, key, value : Linear(embed_dim → embed_dim)
49
+ - out : Linear(embed_dim → embed_dim)
50
+ """
51
+
52
+ def __init__(self, embed_dim: int, num_heads: int, dropout_rate: float = 0.1):
53
+ super().__init__()
54
+ assert embed_dim % num_heads == 0, "embed_dim doit être divisible par num_heads"
55
+
56
+ self.embed_dim = embed_dim
57
+ self.num_heads = num_heads
58
+ self.head_dim = embed_dim // num_heads
59
+
60
+ self.query = nn.Linear(embed_dim, embed_dim)
61
+ self.key = nn.Linear(embed_dim, embed_dim)
62
+ self.value = nn.Linear(embed_dim, embed_dim)
63
+ self.out = nn.Linear(embed_dim, embed_dim)
64
+
65
+ self.dropout = nn.Dropout(dropout_rate)
66
+
67
+ def _scaled_dot_product_attention(
68
+ self,
69
+ q: torch.Tensor,
70
+ k: torch.Tensor,
71
+ v: torch.Tensor,
72
+ mask: Optional[torch.Tensor] = None,
73
+ ) -> torch.Tensor:
74
+ """
75
+ q, k, v : (batch, num_heads, seq_len, head_dim)
76
+ mask : (seq_len, seq_len) ou (batch, 1, seq_len, seq_len)
77
+ """
78
+ dk = q.size(-1)
79
+ scores = torch.matmul(q, k.transpose(-2, -1)) / np.sqrt(dk) # (b, h, L, L)
80
+
81
+ if mask is not None:
82
+ # mask == 0 → -inf
83
+ scores = scores.masked_fill(mask == 0, float("-inf"))
84
+
85
+ attn_weights = torch.softmax(scores, dim=-1)
86
+ attn_weights = self.dropout(attn_weights)
87
+ output = torch.matmul(attn_weights, v) # (b, h, L, head_dim)
88
+ return output
89
+
90
+ def forward(self, x: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
91
+ """
92
+ x : (batch, seq_len, embed_dim)
93
+ """
94
+ batch_size, seq_len, _ = x.size()
95
+
96
+ # Projections linéaires
97
+ q = self.query(x) # (b, L, d)
98
+ k = self.key(x)
99
+ v = self.value(x)
100
+
101
+ # Split en têtes : (b, L, h, d_h) → (b, h, L, d_h)
102
+ def split_heads(t):
103
+ return t.view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
104
+
105
+ q = split_heads(q)
106
+ k = split_heads(k)
107
+ v = split_heads(v)
108
+
109
+ # Attention avec masque causal éventuel
110
+ attn_output = self._scaled_dot_product_attention(q, k, v, mask=mask)
111
+
112
+ # Merge heads : (b, h, L, d_h) → (b, L, d)
113
+ attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len, self.embed_dim)
114
+
115
+ # Projection finale
116
+ out = self.out(attn_output) # (b, L, d)
117
+ return out
118
+
119
+
120
+ class TransformerBlock(nn.Module):
121
+ """
122
+ Bloc Transformer standard :
123
+ - MultiHeadSelfAttention
124
+ - Add & Norm
125
+ - FFN (Linear → ReLU → Linear)
126
+ - Add & Norm
127
+ """
128
+
129
+ def __init__(
130
+ self,
131
+ embed_dim: int,
132
+ num_heads: int,
133
+ ff_dim: int,
134
+ dropout_rate: float = 0.1,
135
+ ):
136
+ super().__init__()
137
+ self.att = MultiHeadSelfAttention(embed_dim, num_heads, dropout_rate=dropout_rate)
138
+ self.ffn = nn.Sequential(
139
+ nn.Linear(embed_dim, ff_dim),
140
+ nn.ReLU(),
141
+ nn.Linear(ff_dim, embed_dim),
142
+ )
143
+
144
+ self.layernorm1 = nn.LayerNorm(embed_dim)
145
+ self.layernorm2 = nn.LayerNorm(embed_dim)
146
+ self.dropout_att = nn.Dropout(dropout_rate)
147
+ self.dropout_ffn = nn.Dropout(dropout_rate)
148
+
149
+ def _causal_mask(self, seq_len: int, device: torch.device) -> torch.Tensor:
150
+ """
151
+ Masque triangulaire inférieur (causal) de taille (1, 1, seq_len, seq_len)
152
+ compatible avec la forme (batch, num_heads, L, L).
153
+ """
154
+ mask = torch.tril(torch.ones((seq_len, seq_len), device=device)).unsqueeze(0).unsqueeze(0)
155
+ return mask # (1, 1, L, L)
156
+
157
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
158
+ """
159
+ x : (batch, seq_len, embed_dim)
160
+ """
161
+ seq_len = x.size(1)
162
+ device = x.device
163
+
164
+ # Masque causal L x L
165
+ mask = self._causal_mask(seq_len, device)
166
+
167
+ # Self-attention + residual
168
+ attn_output = self.att(x, mask=mask)
169
+ x = self.layernorm1(x + self.dropout_att(attn_output))
170
+
171
+ # FFN + residual
172
+ ffn_output = self.ffn(x)
173
+ x = self.layernorm2(x + self.dropout_ffn(ffn_output))
174
+
175
+ return x
176
+
177
+
178
+ class TransformerLanguageModel(nn.Module):
179
+ """
180
+ Modèle PyTorch complet pour la génération de texte (version du notebook v2).
181
+
182
+ Architecture :
183
+ Input (tokens ids) →
184
+ PositionalEmbedding →
185
+ [TransformerBlock] × N →
186
+ Dropout →
187
+ Linear(embed_dim → vocab_size) sur la DERNIÈRE position
188
+ """
189
+
190
+ def __init__(
191
+ self,
192
+ vocab_size: int,
193
+ max_length: int,
194
+ embed_dim: int,
195
+ num_heads: int,
196
+ ff_dim: int,
197
+ num_blocks: int,
198
+ dropout_rate: float = 0.1,
199
+ ):
200
+ super().__init__()
201
+ self.vocab_size = vocab_size
202
+ self.max_length = max_length
203
+ self.embed_dim = embed_dim
204
+ self.num_heads = num_heads
205
+ self.ff_dim = ff_dim
206
+ self.num_blocks = num_blocks
207
+ self.dropout_rate = dropout_rate
208
+
209
+ self.embedding = PositionalEmbedding(vocab_size, embed_dim, max_length)
210
+ self.blocks = nn.ModuleList(
211
+ [
212
+ TransformerBlock(
213
+ embed_dim=embed_dim,
214
+ num_heads=num_heads,
215
+ ff_dim=ff_dim,
216
+ dropout_rate=dropout_rate,
217
+ )
218
+ for _ in range(num_blocks)
219
+ ]
220
+ )
221
+ self.dropout = nn.Dropout(dropout_rate)
222
+ self.fc_out = nn.Linear(embed_dim, vocab_size)
223
+
224
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
225
+ """
226
+ x : (batch, seq_len) → logits : (batch, vocab_size) sur la dernière position.
227
+ """
228
+ # Embedding (batch, seq_len, embed_dim)
229
+ x = self.embedding(x)
230
+
231
+ # Empilement des blocs Transformer
232
+ for block in self.blocks:
233
+ x = block(x)
234
+
235
+ # Dropout global
236
+ x = self.dropout(x)
237
+
238
+ # On ne garde que la dernière position
239
+ last_token = x[:, -1, :] # (batch, embed_dim)
240
+
241
+ # Logits vocabulaire
242
+ logits = self.fc_out(last_token) # (batch, vocab_size)
243
+ return logits
244
+
245
+
246
+ # ======================================================================
247
+ # GÉNÉRATEUR DE TEXTE : TransformerTextGenerator
248
+ # ======================================================================
249
+
250
+
251
+ class TransformerTextGenerator:
252
+ """
253
+ Générateur de texte pour le modèle Transformer (PyTorch, checkpoint v2).
254
+
255
+ - Charge le corpus texte depuis PROJECT_ROOT / data/raw/nlp
256
+ - Nettoie le texte comme dans le notebook
257
+ - Reconstruit le tokenizer (word-level) identique
258
+ - Reconstruit le modèle PyTorch si on reçoit un state_dict (OrderedDict)
259
+ - Génère du texte en mode auto-régressif à partir d'un seed.
260
+ """
261
+
262
+ # Singleton interne
263
+ _instance: "TransformerTextGenerator | None" = None
264
+
265
+ # Références au projet / corpus
266
+ PROJECT_ROOT = Path(__file__).resolve().parents[2]
267
+ CORPUS_DIR = PROJECT_ROOT / "gradio" / "data"
268
+ CORPUS_LENGTH_PARAM = "_car_FULL_" # même filtre que dans le notebook FULL_50
269
+
270
+ # Longueur de séquence par défaut (notebook FULL_50)
271
+ DEFAULT_MAX_LENGTH = 50
272
+
273
+ def __init__(self, model, tokenizer: Tokenizer, max_length: int):
274
+ # Ici, on veut un nn.Module, pas un state_dict
275
+ if isinstance(model, (dict, OrderedDict)):
276
+ raise TypeError(
277
+ "TransformerTextGenerator a reçu un 'state_dict' (OrderedDict) au lieu d'un modèle. "
278
+ "Reconstruction du modèle attendue AVANT l'instanciation."
279
+ )
280
+
281
+ self.model = model
282
+ self.tokenizer = tokenizer
283
+ self.max_length = max_length
284
+
285
+ # Backend: PyTorch ou Keras (théorique, mais ici on est en PyTorch)
286
+ self._is_torch = isinstance(self.model, nn.Module)
287
+ if self._is_torch:
288
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
289
+ self.model.to(self.device)
290
+ self.model.eval()
291
+ else:
292
+ self.device = None
293
+
294
+ # ------------------------------------------------------------------
295
+ # Méthodes de classe : singleton / factory
296
+ # ------------------------------------------------------------------
297
+ @classmethod
298
+ def get_instance(cls, model_or_state) -> "TransformerTextGenerator":
299
+ """
300
+ Retourne une unique instance, basée sur :
301
+ - un modèle Transformer PyTorch déjà construit (nn.Module)
302
+ - OU un state_dict (OrderedDict) provenant du .pt sauvegardé.
303
+ """
304
+
305
+ if cls._instance is not None:
306
+ return cls._instance
307
+
308
+ # 1) Reconstruire le modèle PyTorch si on reçoit un state_dict
309
+ if isinstance(model_or_state, (dict, OrderedDict)):
310
+ torch_model = cls._build_torch_model_from_state_dict(model_or_state)
311
+ else:
312
+ # On suppose ici un nn.Module déjà prêt (PyTorch)
313
+ torch_model = model_or_state
314
+
315
+ # 2) Charger + nettoyer le corpus, rebuild du tokenizer
316
+ full_text_clean = cls._load_full_clean_corpus()
317
+ tokenizer = cls._build_tokenizer_from_corpus(full_text_clean)
318
+
319
+ # 3) max_length : si le modèle l'expose, on le récupère, sinon fallback
320
+ max_length = getattr(torch_model, "max_length", cls.DEFAULT_MAX_LENGTH)
321
+
322
+ cls._instance = cls(
323
+ model=torch_model,
324
+ tokenizer=tokenizer,
325
+ max_length=max_length,
326
+ )
327
+ return cls._instance
328
+
329
+ # ------------------------------------------------------------------
330
+ # Reconstruction du modèle PyTorch depuis le state_dict
331
+ # ------------------------------------------------------------------
332
+ @classmethod
333
+ def _build_torch_model_from_state_dict(
334
+ cls,
335
+ state: Mapping[str, torch.Tensor],
336
+ ) -> TransformerLanguageModel:
337
+ """
338
+ Reconstruit un `TransformerLanguageModel` à partir d’un state_dict
339
+ tel que sauvegardé dans le notebook transformer_trainme_v2.
340
+
341
+ On infère les hyperparamètres structurants à partir des shapes :
342
+ - vocab_size : dim 0 de embedding.token_emb.weight
343
+ - embed_dim : dim 1 de embedding.token_emb.weight
344
+ - ff_dim : dim 0 de blocks.0.ffn.0.weight
345
+ - num_blocks : nombre de blocs présents dans `blocks.{i}.att.query.weight`
346
+ - num_heads : on reprend la valeur du notebook (4)
347
+ - max_length : on utilise DEFAULT_MAX_LENGTH (50) utilisée lors du training.
348
+ """
349
+
350
+ # Vérifications de base
351
+ if "embedding.token_emb.weight" not in state:
352
+ raise KeyError(
353
+ "Le state_dict fourni ne contient pas la clé 'embedding.token_emb.weight'. "
354
+ "Vérifie que tu utilises bien le checkpoint du TransformerLanguageModel v2."
355
+ )
356
+
357
+ # vocab_size, embed_dim
358
+ token_emb_weight = state["embedding.token_emb.weight"]
359
+ vocab_size = token_emb_weight.shape[0]
360
+ embed_dim = token_emb_weight.shape[1]
361
+
362
+ # ff_dim
363
+ ffn0_key = "blocks.0.ffn.0.weight"
364
+ if ffn0_key not in state:
365
+ raise KeyError(
366
+ f"Clé '{ffn0_key}' absente du state_dict. "
367
+ "La structure attendue est blocks.{i}.ffn.0.weight."
368
+ )
369
+ ff_dim = state[ffn0_key].shape[0]
370
+
371
+ # Nombre de blocs
372
+ num_blocks = 0
373
+ while f"blocks.{num_blocks}.att.query.weight" in state:
374
+ num_blocks += 1
375
+ if num_blocks == 0:
376
+ raise ValueError(
377
+ "Impossible de déterminer le nombre de blocs Transformer "
378
+ "(aucune clé 'blocks.{i}.att.query.weight' trouvée)."
379
+ )
380
+
381
+ # Dans le notebook v2, num_heads = 4
382
+ num_heads = 4
383
+
384
+ # max_length : utilisé pour le positional encoding, non paramétrique
385
+ max_length = cls.DEFAULT_MAX_LENGTH
386
+
387
+ # Dropout rate standard du notebook
388
+ dropout_rate = 0.1
389
+
390
+ # Construction du modèle
391
+ model = TransformerLanguageModel(
392
+ vocab_size=vocab_size,
393
+ max_length=max_length,
394
+ embed_dim=embed_dim,
395
+ num_heads=num_heads,
396
+ ff_dim=ff_dim,
397
+ num_blocks=num_blocks,
398
+ dropout_rate=dropout_rate,
399
+ )
400
+
401
+ # Chargement des poids
402
+ model.load_state_dict(state)
403
+ return model
404
+
405
+ # ------------------------------------------------------------------
406
+ # Chargement du corpus & nettoyage
407
+ # ------------------------------------------------------------------
408
+ @classmethod
409
+ def _load_full_clean_corpus(cls) -> str:
410
+ """
411
+ Reproduit la logique du notebook :
412
+
413
+ - charge les .txt dans CORPUS_DIR
414
+ - filtre avec CORPUS_LENGTH_PARAM
415
+ - concatène
416
+ - applique clean_text(...)
417
+ """
418
+ corpus_dir = cls.CORPUS_DIR
419
+ if not corpus_dir.exists():
420
+ raise FileNotFoundError(f"Corpus directory not found: {corpus_dir}")
421
+
422
+ corpus_paths = sorted(
423
+ p
424
+ for p in corpus_dir.glob("*.txt")
425
+ if (not cls.CORPUS_LENGTH_PARAM or cls.CORPUS_LENGTH_PARAM in p.name)
426
+ )
427
+
428
+ if not corpus_paths:
429
+ raise FileNotFoundError(
430
+ f"No corpus .txt files found in {corpus_dir} "
431
+ f"(filter='{cls.CORPUS_LENGTH_PARAM}')"
432
+ )
433
+
434
+ corpus_texts = []
435
+ for path in corpus_paths:
436
+ with open(path, encoding="utf-8") as f:
437
+ corpus_texts.append(f.read())
438
+
439
+ full_text = "\n".join(text.strip() for text in corpus_texts)
440
+ full_text_clean = cls.clean_text(full_text)
441
+ return full_text_clean
442
+
443
+ @staticmethod
444
+ def clean_text(texte: str) -> str:
445
+ """Nettoie et normalise le texte (version notebook)."""
446
+ texte = texte.lower()
447
+ texte = texte.replace("'", "")
448
+ texte = re.sub(r"([.,!?])", r" \1 ", texte)
449
+ texte = re.sub(r"\s+", " ", texte)
450
+ return texte.strip()
451
+
452
+ # ------------------------------------------------------------------
453
+ # Tokenizer
454
+ # ------------------------------------------------------------------
455
+ @staticmethod
456
+ def _build_tokenizer_from_corpus(full_text_clean: str) -> Tokenizer:
457
+ """
458
+ Recrée le tokenizer EXACTEMENT comme dans le notebook :
459
+
460
+ tokenizer = Tokenizer(filters='', lower=False, oov_token='<UNK>')
461
+ tokenizer.fit_on_texts([full_text_clean])
462
+ """
463
+ tok = Tokenizer(filters="", lower=False, oov_token="<UNK>")
464
+ tok.fit_on_texts([full_text_clean])
465
+ return tok
466
+
467
+ def _encode_prompt(self, prompt: str) -> list[int]:
468
+ """Nettoie le prompt et le convertit en liste d’IDs tokens."""
469
+ prompt_clean = self.clean_text(prompt)
470
+ token_list = self.tokenizer.texts_to_sequences([prompt_clean])[0]
471
+ return token_list
472
+
473
+ # ------------------------------------------------------------------
474
+ # Backend-unified prediction
475
+ # ------------------------------------------------------------------
476
+ def _predict_proba(self, sequence: np.ndarray) -> np.ndarray:
477
+ """
478
+ Unifie la prédiction entre Keras (.predict) et PyTorch (.forward).
479
+ Retourne un vecteur de probabilités sur le vocabulaire.
480
+ """
481
+ if self._is_torch:
482
+ x = torch.from_numpy(sequence).long().to(self.device)
483
+ with torch.no_grad():
484
+ logits = self.model(x)
485
+ # Ici, logit final : (batch, vocab_size)
486
+ if logits.dim() == 2:
487
+ logits = logits[0] # (vocab_size,)
488
+ else:
489
+ # fallback très défensif
490
+ logits = logits.view(-1)
491
+ probs = torch.softmax(logits, dim=-1).cpu().numpy()
492
+ return probs
493
+ else:
494
+ # Chemin Keras théorique (non utilisé dans v2)
495
+ return self.model.predict(sequence, verbose=0)[0]
496
+
497
+ # ------------------------------------------------------------------
498
+ # Génération
499
+ # ------------------------------------------------------------------
500
+ def generate_text(
501
+ self,
502
+ seed_text: str,
503
+ num_words: int = 60,
504
+ temperature: float = 1.0,
505
+ seed: Optional[int] = None,
506
+ ) -> str:
507
+ """
508
+ Génère du texte à partir d’un prompt initial, en mode auto-régressif.
509
+
510
+ - temperature contrôle la créativité
511
+ - num_words = nombre de tokens supplémentaires à générer
512
+ """
513
+ seed_text = seed_text.strip()
514
+ if not seed_text:
515
+ return ""
516
+
517
+ rng = np.random.default_rng(seed) if seed is not None else np.random
518
+
519
+ generated_text = seed_text
520
+ token_list = self._encode_prompt(seed_text)
521
+
522
+ for _ in range(num_words):
523
+ if not token_list:
524
+ break
525
+
526
+ # Padding / tronquage à max_length-1 comme au training
527
+ sequence = pad_sequences(
528
+ [token_list],
529
+ maxlen=self.max_length - 1,
530
+ padding="pre",
531
+ )
532
+
533
+ # Prédiction du prochain token (probas)
534
+ preds = self._predict_proba(sequence.astype("int64"))
535
+
536
+ # Température
537
+ preds = np.log(preds + 1e-7) / temperature
538
+ preds = np.exp(preds) / np.sum(np.exp(preds))
539
+
540
+ # Échantillonnage
541
+ next_id = rng.choice(len(preds), p=preds)
542
+
543
+ # Décodage ID -> mot
544
+ word = self.tokenizer.index_word.get(next_id, "")
545
+
546
+ if not word:
547
+ continue
548
+
549
+ generated_text += " " + word
550
+ token_list.append(next_id)
551
+
552
+ return generated_text
src/gradio/helpers/custom_layers.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import tensorflow as tf
3
+ from tensorflow import keras
4
+ from tensorflow.keras import layers
5
+
6
+ # Pour la sérialisation propre des couches custom
7
+ try:
8
+ from keras.saving import register_keras_serializable
9
+ except ImportError:
10
+ from tensorflow.keras.utils import register_keras_serializable
11
+
12
+
13
+ def get_positional_encoding(seq_len, d_model):
14
+ """
15
+ Crée le positional encoding pour le Transformer (sinus/cosinus).
16
+ """
17
+ positions = np.arange(seq_len)[:, np.newaxis]
18
+ dimensions = np.arange(d_model)[np.newaxis, :]
19
+ angle_rates = 1 / np.power(10000, (2 * (dimensions // 2)) / np.float32(d_model))
20
+ angle_rads = positions * angle_rates
21
+
22
+ pos_encoding = np.zeros((seq_len, d_model))
23
+ pos_encoding[:, 0::2] = np.sin(angle_rads[:, 0::2])
24
+ pos_encoding[:, 1::2] = np.cos(angle_rads[:, 1::2])
25
+ return tf.cast(pos_encoding[np.newaxis, ...], dtype=tf.float32)
26
+
27
+
28
+ @register_keras_serializable(package="trainme")
29
+ class MultiHeadSelfAttention(layers.Layer):
30
+ """
31
+ Implémentation du Multi-Head Self-Attention (causal).
32
+ Même logique que dans le notebook d'entraînement.
33
+ """
34
+ def __init__(self, embed_dim, num_heads, **kwargs):
35
+ super(MultiHeadSelfAttention, self).__init__(**kwargs)
36
+ self.embed_dim = embed_dim
37
+ self.num_heads = num_heads
38
+
39
+ assert embed_dim % num_heads == 0, "embed_dim doit être divisible par num_heads"
40
+
41
+ self.projection_dim = embed_dim // num_heads
42
+
43
+ # Projections linéaires pour Q, K, V
44
+ self.query_dense = layers.Dense(embed_dim, name="query")
45
+ self.key_dense = layers.Dense(embed_dim, name="key")
46
+ self.value_dense = layers.Dense(embed_dim, name="value")
47
+
48
+ # Projection finale
49
+ self.combine_heads = layers.Dense(embed_dim, name="output")
50
+
51
+ def attention(self, query, key, value):
52
+ """Calcule l'attention scaled dot-product avec masque de causalité."""
53
+ score = tf.matmul(query, key, transpose_b=True)
54
+ dim_key = tf.cast(tf.shape(key)[-1], tf.float32)
55
+ scaled_score = score / tf.math.sqrt(dim_key)
56
+
57
+ # Masque de causalité (look-ahead mask)
58
+ seq_len = tf.shape(scaled_score)[-1]
59
+ mask = 1 - tf.linalg.band_part(tf.ones((seq_len, seq_len)), -1, 0)
60
+ mask = mask * -1e9 # -inf sur les positions futures
61
+
62
+ scaled_score += mask
63
+
64
+ weights = tf.nn.softmax(scaled_score, axis=-1)
65
+ output = tf.matmul(weights, value)
66
+ return output, weights
67
+
68
+ def separate_heads(self, x, batch_size):
69
+ """Sépare la dernière dimension en (num_heads, projection_dim)."""
70
+ x = tf.reshape(x, (batch_size, -1, self.num_heads, self.projection_dim))
71
+ return tf.transpose(x, perm=[0, 2, 1, 3])
72
+
73
+ def call(self, inputs):
74
+ batch_size = tf.shape(inputs)[0]
75
+
76
+ # Projections linéaires
77
+ query = self.query_dense(inputs)
78
+ key = self.key_dense(inputs)
79
+ value = self.value_dense(inputs)
80
+
81
+ # Séparer en multiple heads
82
+ query = self.separate_heads(query, batch_size)
83
+ key = self.separate_heads(key, batch_size)
84
+ value = self.separate_heads(value, batch_size)
85
+
86
+ # Attention avec masque de causalité
87
+ attention, weights = self.attention(query, key, value)
88
+
89
+ # Recombiner les heads
90
+ attention = tf.transpose(attention, perm=[0, 2, 1, 3])
91
+ concat_attention = tf.reshape(attention, (batch_size, -1, self.embed_dim))
92
+
93
+ # Projection finale
94
+ output = self.combine_heads(concat_attention)
95
+ return output
96
+
97
+ def get_config(self):
98
+ config = super().get_config()
99
+ config.update(
100
+ {
101
+ "embed_dim": self.embed_dim,
102
+ "num_heads": self.num_heads,
103
+ }
104
+ )
105
+ return config
106
+
107
+
108
+ @register_keras_serializable(package="trainme")
109
+ class TransformerBlock(layers.Layer):
110
+ """
111
+ Un bloc Transformer complet, identique au notebook :
112
+ - Multi-Head Self-Attention (custom)
113
+ - Feed-Forward Network
114
+ - Layer Normalization
115
+ - Residual Connections
116
+ """
117
+ def __init__(self, embed_dim, num_heads, ff_dim, dropout_rate=0.1, **kwargs):
118
+ super(TransformerBlock, self).__init__(**kwargs)
119
+ self.embed_dim = embed_dim
120
+ self.num_heads = num_heads
121
+ self.ff_dim = ff_dim
122
+ self.dropout_rate = dropout_rate
123
+
124
+ # Multi-Head Attention (ta classe custom)
125
+ self.att = MultiHeadSelfAttention(embed_dim, num_heads)
126
+
127
+ # Feed-Forward Network
128
+ self.ffn = keras.Sequential(
129
+ [
130
+ layers.Dense(ff_dim, activation="relu"),
131
+ layers.Dense(embed_dim),
132
+ ]
133
+ )
134
+
135
+ # Layer Normalization
136
+ self.layernorm1 = layers.LayerNormalization(epsilon=1e-6)
137
+ self.layernorm2 = layers.LayerNormalization(epsilon=1e-6)
138
+
139
+ # Dropout
140
+ self.dropout1 = layers.Dropout(dropout_rate)
141
+ self.dropout2 = layers.Dropout(dropout_rate)
142
+
143
+ def call(self, inputs, training=False):
144
+ # Multi-Head Attention avec residual connection
145
+ attn_output = self.att(inputs)
146
+ attn_output = self.dropout1(attn_output, training=training)
147
+ out1 = self.layernorm1(inputs + attn_output)
148
+
149
+ # Feed-Forward Network avec residual connection
150
+ ffn_output = self.ffn(out1)
151
+ ffn_output = self.dropout2(ffn_output, training=training)
152
+ out2 = self.layernorm2(out1 + ffn_output)
153
+
154
+ return out2
155
+
156
+ def get_config(self):
157
+ config = super().get_config()
158
+ config.update(
159
+ {
160
+ "embed_dim": self.embed_dim,
161
+ "num_heads": self.num_heads,
162
+ "ff_dim": self.ff_dim,
163
+ "dropout_rate": self.dropout_rate,
164
+ }
165
+ )
166
+ return config
167
+
168
+
169
+ @register_keras_serializable(package="trainme")
170
+ class PositionalEmbedding(layers.Layer):
171
+ """
172
+ Combine l'embedding des tokens avec le positional encoding.
173
+ Identique au notebook, avec compat sérialisation.
174
+ """
175
+ def __init__(self, vocab_size, embed_dim, max_len, **kwargs):
176
+ super(PositionalEmbedding, self).__init__(**kwargs)
177
+ self.vocab_size = vocab_size
178
+ self.embed_dim = embed_dim
179
+ self.max_len = max_len
180
+
181
+ self.token_emb = layers.Embedding(input_dim=vocab_size, output_dim=embed_dim)
182
+ self.pos_encoding = get_positional_encoding(max_len, embed_dim)
183
+
184
+ def call(self, x):
185
+ seq_len = tf.shape(x)[1]
186
+ x = self.token_emb(x)
187
+ # Ajouter le positional encoding
188
+ x = x + self.pos_encoding[:, :seq_len, :]
189
+ return x
190
+
191
+ def get_config(self):
192
+ config = super().get_config()
193
+ config.update(
194
+ {
195
+ "vocab_size": self.vocab_size,
196
+ "embed_dim": self.embed_dim,
197
+ "max_len": self.max_len,
198
+ }
199
+ )
200
+ return config
src/gradio/helpers/exercices_tab_utilis.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from pathlib import Path
3
+ import os
4
+ import pandas as pd
5
+ from typing import Union
6
+
7
+
8
+
9
+ # Le fichier s’exécute depuis son répertoire → on peut repartir du cwd
10
+ current_dir = Path.cwd()
11
+ json_path = current_dir / "src" / "gradio" / "data"
12
+
13
+ # Chemin par défaut vers ton JSON fusionné
14
+ DEFAULT_EXERCICES_PATH = Path(
15
+ os.getenv(
16
+ "DATASET_EXERCICES_FUSION",
17
+ json_path / "dataset_exercices_fusion_20251127_2004.json",
18
+ )
19
+ )
20
+
21
+ def _load_exercices(path: Union[str, Path]) -> pd.DataFrame:
22
+ path = Path(path)
23
+ if not path.exists():
24
+ raise FileNotFoundError(f"Dataset exercices introuvable : {path}")
25
+
26
+ df = pd.read_json(path)
27
+ df = df.reset_index(drop=True)
28
+ return df
29
+
30
+
31
+ # DEFAULT_GOAL_PATH = Path(
32
+ # os.getenv(
33
+ # "GOAL_MAP_PATH",
34
+ # json_path / "goal_map.json",
35
+ # )
36
+ # )
37
+
38
+ # def _load_goals(path: Union[str, Path]) -> list[str]:
39
+ # """
40
+ # Charge la liste des goals depuis goal_map.json.
41
+ # On suppose un JSON de type: ["General Fitness", "Strength", ...]
42
+ # """
43
+ # path = Path(path)
44
+ # if not path.exists():
45
+ # raise FileNotFoundError(f"Goal map introuvable : {path}")
46
+
47
+ # with path.open("r", encoding="utf-8") as f:
48
+ # data = json.load(f)
49
+
50
+ # if isinstance(data, list):
51
+ # # On garde les chaînes, on nettoie le doublons, on trie
52
+ # goals = [str(x) for x in data if x not in (None, "")]
53
+ # return sorted(set(goals))
54
+
55
+ # # fallback simple si jamais c’est un dict: on prend les clés
56
+ # if isinstance(data, dict):
57
+ # return sorted(map(str, data.keys()))
58
+
59
+ # raise ValueError(f"Format inattendu pour goal_map.json : {type(data)}")
60
+
61
+
62
+ # def _sync_level(level_val: str) -> str:
63
+ # # Recopie la valeur du champ 'level_out' du ML tab
64
+ # return level_val or ""
65
+
66
+ # def _filter_by_level(df: pd.DataFrame, level: str) -> pd.DataFrame:
67
+ # """Filtre automatiquement selon difficulty en fonction du niveau ML."""
68
+ # if "difficulty" not in df.columns:
69
+ # return df # sécurité
70
+
71
+ # level = (level or "").strip().lower()
72
+
73
+ # if level == "beginner":
74
+ # allowed = ["beginner"]
75
+ # elif level == "intermediate":
76
+ # allowed = ["intermediate"]
77
+ # elif level == "advanced":
78
+ # allowed = ["advanced"]
79
+ # elif level == "expert":
80
+ # allowed = ["expert"]
81
+ # else: # vide
82
+ # return df
83
+
84
+ # return df[df["difficulty"].str.lower().isin(allowed)]
src/gradio/helpers/log_utils.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import csv
2
+ import pandas as pd
3
+ from pathlib import Path
4
+
5
+ def log_prediction(
6
+ log_dir: Path,
7
+ row_in: dict,
8
+ y_hat: float,
9
+ latency_ms: int,
10
+ model_filename: str,
11
+ model_version: str,
12
+ target_name: str,
13
+ ):
14
+ """
15
+ Enregistre une prédiction dans un fichier CSV de logs.
16
+
17
+ Args:
18
+ log_dir: dossier où écrire le fichier "predictions.csv"
19
+ row_in: dictionnaire des features d'entrée
20
+ y_hat: prédiction numérique
21
+ latency_ms: durée d'inférence en millisecondes
22
+ model_filename: nom du fichier modèle (ex: "model.joblib")
23
+ model_version: version du modèle (issue du schéma)
24
+ target_name: nom de la variable cible
25
+ """
26
+ log_file = log_dir / "predictions.csv"
27
+ write_header = not log_file.exists()
28
+
29
+ new_row = {
30
+ "ts": pd.Timestamp.utcnow().isoformat(),
31
+ **row_in,
32
+ target_name: y_hat,
33
+ "latency_ms": latency_ms,
34
+ "model_file": model_filename,
35
+ "model_version": model_version,
36
+ }
37
+
38
+ with log_file.open("a", newline="", encoding="utf-8") as f:
39
+ writer = csv.DictWriter(f, fieldnames=list(new_row.keys()))
40
+ if write_header:
41
+ writer.writeheader()
42
+ writer.writerow(new_row)
src/gradio/helpers/model_manager.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # from pathlib import Path
2
+ # import pandas as pd
3
+ # import json
4
+ # import gpt_2_simple as gpt2
5
+ # import tensorflow.compat.v1 as tf
6
+
7
+ # tf.disable_v2_behavior()
8
+
9
+ # from ..generators.gpt2_distillation_text_generator import GPT2_DistilledTextGenerator
10
+ # from ..generators.gpt2_fine_tuning_text_generator import GPT2_FineTuningTextGenerator
11
+ # from ..generators.transformer_text_generator import TransformerTextGenerator
12
+ # from ..generators.lstm_text_generator import LSTMTextGenerator
13
+
14
+ # # ---------------------------------------------------------------------
15
+ # # Définition des chemins principaux
16
+ # # ---------------------------------------------------------------------
17
+
18
+ # PROJECT_ROOT = Path(__file__).resolve().parents[2]
19
+ # MODEL_DIR = PROJECT_ROOT / "models" / "v1"
20
+
21
+ # # ---------------------------------------------------------------------
22
+ # # Registre des modèles DL
23
+ # # ---------------------------------------------------------------------
24
+
25
+ # MODEL_REGISTRY = {
26
+ # "xMas - GPT2 Fine-tuning": {
27
+ # "type": "gpt2",
28
+ # # "path": MODEL_DIR / "gpt2_trainme_distillation_gpt2_v5",
29
+ # "path": MODEL_DIR / "gpt2-xmas-finetuning_run3",
30
+ # "report_path": MODEL_DIR / "GPT2_Fine_Tuning_model_report_gpt_2_simple.json",
31
+ # },
32
+ # }
33
+
34
+ # # Cache interne pour éviter de recharger plusieurs fois
35
+ # LOADED_MODELS = {}
36
+ # LOADED_TOKENIZERS = {}
37
+
38
+
39
+ # # ---------------------------------------------------------------------
40
+ # # Fonction appelée par ton événement Gradio lors du changement de modèle
41
+ # # ---------------------------------------------------------------------
42
+ # def on_model_change(model_name: str) -> str:
43
+ # """
44
+ # Charge le modèle NLP correspondant au nom sélectionné.
45
+ # Retourne simplement le chemin du modèle chargé (affiché dans l'UI).
46
+ # """
47
+
48
+ # info = MODEL_REGISTRY[model_name]
49
+ # model_path = info["path"]
50
+ # if isinstance(model_path, str):
51
+ # model_path = Path(model_path)
52
+
53
+ # checkpoint_dir = str(model_path.parent)
54
+ # run_name = model_path.name
55
+
56
+ # # Déjà chargé ? On renvoie direct.
57
+ # if model_name in LOADED_MODELS:
58
+ # return f"Model loaded from cache: {model_path}"
59
+
60
+ # tf.reset_default_graph()
61
+ # sess = gpt2.start_tf_sess()
62
+ # gpt2.load_gpt2(
63
+ # sess,
64
+ # checkpoint_dir=checkpoint_dir,
65
+ # run_name=run_name,
66
+ # )
67
+
68
+ # # On pourrait stocker la session si besoin plus tard
69
+ # LOADED_MODELS[model_name] = {
70
+ # "sess": sess,
71
+ # "checkpoint_dir": checkpoint_dir,
72
+ # "run_name": run_name,
73
+ # }
74
+
75
+ # return f"Model loaded: {model_path}"
76
+
77
+
78
+ # def clean_special_tokens(text: str) -> str:
79
+ # return (
80
+ # text.replace("<|startoftext|>", "")
81
+ # .replace("<|endoftext|>", "")
82
+ # .strip()
83
+ # )
84
+
85
+
86
+ # def cut_to_last_sentence(text: str, min_chars: int = 60) -> str:
87
+ # """
88
+ # Coupe le texte au dernier '.', '!' ou '?' pour éviter
89
+ # les phrases tronquées. min_chars évite de couper trop tôt.
90
+ # """
91
+ # last_dot = text.rfind(".")
92
+ # last_exc = text.rfind("!")
93
+ # last_q = text.rfind("?")
94
+ # end_idx = max(last_dot, last_exc, last_q)
95
+
96
+ # if end_idx != -1 and end_idx >= min_chars:
97
+ # return text[: end_idx + 1].strip()
98
+ # return text.strip()
99
+
100
+
101
+ # def generate_clean_program(
102
+ # sess,
103
+ # prompt: str,
104
+ # max_length: int = 200,
105
+ # temperature: float = 0.8,
106
+ # checkpoint_dir: str | None = None,
107
+ # run_name: str | None = None,
108
+ # ) -> str:
109
+ # """
110
+ # Génère un texte brut avec gpt_2_simple en forçant checkpoint_dir / run_name,
111
+ # puis applique le nettoyage spécifique TrAIn.me.
112
+ # """
113
+ # gen_kwargs = dict(
114
+ # sess=sess,
115
+ # prefix=prompt,
116
+ # length=max_length,
117
+ # temperature=temperature,
118
+ # top_k=40,
119
+ # nsamples=1,
120
+ # batch_size=1,
121
+ # return_as_list=True,
122
+ # truncate="<|endoftext|>", # s'arrête si le token apparaît
123
+ # )
124
+
125
+ # # IMPORTANT : on force les chemins si fournis
126
+ # if checkpoint_dir is not None:
127
+ # gen_kwargs["checkpoint_dir"] = checkpoint_dir
128
+ # if run_name is not None:
129
+ # gen_kwargs["run_name"] = run_name
130
+
131
+ # raw_list = gpt2.generate(**gen_kwargs)
132
+ # raw = raw_list[0] if isinstance(raw_list, list) else raw_list
133
+
134
+ # txt = clean_special_tokens(raw)
135
+ # txt = cut_to_last_sentence(txt)
136
+ # return txt
137
+
138
+
139
+ # def generate_text_with_model(model_name: str, prompt: str) -> str:
140
+ # """Génère du texte avec le modèle sélectionné à partir du prompt."""
141
+ # prompt = prompt.strip()
142
+ # if not prompt:
143
+ # return "Please enter a prompt before generating."
144
+
145
+ # info = MODEL_REGISTRY[model_name]
146
+ # model_path = info["path"] # Path vers le dossier qui contient encoder.json / model-xxx
147
+ # if isinstance(model_path, str):
148
+ # model_path = Path(model_path)
149
+
150
+ # checkpoint_dir = str(model_path.parent) # ex: .../src/models/v1
151
+ # run_name = model_path.name # ex: "gpt2-xmas-finetuning_run3"
152
+
153
+ # print(f"[GPT-2] Using checkpoint_dir={checkpoint_dir} run_name={run_name}")
154
+
155
+ # # Reset du graphe TF + session
156
+ # tf.reset_default_graph()
157
+ # sess = gpt2.start_tf_sess()
158
+
159
+ # # On indique explicitement où est le modèle
160
+ # gpt2.load_gpt2(
161
+ # sess,
162
+ # checkpoint_dir=checkpoint_dir,
163
+ # run_name=run_name,
164
+ # )
165
+
166
+ # text = generate_clean_program(
167
+ # sess,
168
+ # prompt=prompt,
169
+ # max_length=220,
170
+ # temperature=0.7,
171
+ # checkpoint_dir=checkpoint_dir,
172
+ # run_name=run_name,
173
+ # )
174
+ # return text
175
+
176
+
177
+ # def get_dl_model_report_components(model_name: str):
178
+ # """
179
+ # Retourne 4 DataFrames Gradio-ready :
180
+ # - Summary
181
+ # - Model
182
+ # - Training
183
+ # - Metrics
184
+
185
+ # Si pas de rapport → retourne les DF vides.
186
+ # """
187
+ # info = MODEL_REGISTRY.get(model_name)
188
+ # if not info:
189
+ # return _empty_dl_dfs()
190
+
191
+ # report_path = info.get("report_path")
192
+ # if not report_path or not report_path.exists():
193
+ # return _empty_dl_dfs()
194
+
195
+ # try:
196
+ # data = json.loads(report_path.read_text(encoding="utf-8"))
197
+ # except Exception as e:
198
+ # print(f"[DL REPORT] Error while reading {report_path}: {e}")
199
+ # return _empty_dl_dfs()
200
+
201
+ # # ===== Summary =====
202
+ # dataset = data.get("dataset", {})
203
+
204
+ # summary_rows = [
205
+ # ("created_at", data.get("created_at", "")),
206
+ # ("task", data.get("task", "")),
207
+ # ("target", data.get("target", "")),
208
+ # ("framework", data.get("framework", "")),
209
+ # ("dataset.file", dataset.get("file", "")),
210
+ # ("dataset.size_bytes", dataset.get("size_bytes", "")),
211
+ # ("dataset.tokens", dataset.get("tokens", "")),
212
+ # ]
213
+
214
+ # df_summary = pd.DataFrame(summary_rows, columns=["Key", "Value"])
215
+
216
+ # # ===== Model config =====
217
+ # model_cfg = data.get("model", {}) or {}
218
+ # df_model = pd.DataFrame(
219
+ # [(k, v) for k, v in model_cfg.items()],
220
+ # columns=["Key", "Value"],
221
+ # )
222
+
223
+ # # ===== Training =====
224
+ # training_cfg = data.get("training", {}) or {}
225
+ # df_training = pd.DataFrame(
226
+ # [(k, v) for k, v in training_cfg.items()],
227
+ # columns=["Key", "Value"],
228
+ # )
229
+
230
+ # # ===== Metrics =====
231
+ # metrics_cfg = data.get("metrics", {}) or {}
232
+ # df_metrics = pd.DataFrame(
233
+ # [(k, v) for k, v in metrics_cfg.items()],
234
+ # columns=["Metric", "Value"],
235
+ # )
236
+
237
+ # return df_summary, df_model, df_training, df_metrics
238
+
239
+
240
+
241
+ # def _empty_dl_dfs():
242
+ # """Retourne 4 DataFrames vides pour éviter les erreurs."""
243
+ # empty = pd.DataFrame({"Key": [], "Value": []})
244
+ # empty_metrics = pd.DataFrame({"Metric": [], "Value": []})
245
+ # return empty, empty, empty, empty_metrics
src/gradio/helpers/predict_utils.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import pandas as pd
3
+ from pathlib import Path
4
+
5
+ from .log_utils import log_prediction
6
+ from .preprocess_utils import maybe_apply_feature_scaler, maybe_inverse_target
7
+ from ..model_loader import pipeline_has_scaler
8
+ from typing import Dict, List, Tuple
9
+ import numpy as np
10
+
11
+
12
+ def ui_to_internal_row(
13
+ ui_dict: Dict[str, object],
14
+ expected_cols: List[str],
15
+ encoder,
16
+ ) -> pd.DataFrame:
17
+
18
+ row = {}
19
+
20
+ # === A) GENDER_1.0 ==========================================
21
+ if "Gender_1.0" in expected_cols:
22
+ g_str = ui_dict["Gender"]
23
+ g_df = pd.DataFrame([[g_str]], columns=["Gender"])
24
+ g_encoded = float(encoder.transform(g_df)[0, 0])
25
+ row["Gender_1.0"] = 1.0 if g_encoded == 1.0 else 0.0
26
+
27
+ # === B) WORKOUT_TYPE_* (HIIT / Strength / Yoga / Cardio) ===
28
+ workout_types = ["Cardio", "Strength", "HIIT", "Yoga"]
29
+ selected_wt = ui_dict["Workout_Type"]
30
+
31
+ for wt in workout_types:
32
+ col = f"Workout_Type_{wt}"
33
+ if col in expected_cols:
34
+ row[col] = 1.0 if selected_wt == wt else 0.0
35
+
36
+ # # === C) BODY_PART_* (One-Hot) ===============================
37
+ # body_parts = ["Abs", "Arms", "Back", "Chest", "Forearms", "Legs", "Shoulders"]
38
+ # selected_bp = ui_dict["Body Part"]
39
+
40
+ # for bp in body_parts:
41
+ # col = f"Body Part_{bp}"
42
+ # if col in expected_cols:
43
+ # row[col] = 1.0 if selected_bp == bp else 0.0
44
+
45
+
46
+ # === E) COPIE DIRECTE DES AUTRES COLONNES ===================
47
+ for col in expected_cols:
48
+ if col in row:
49
+ continue
50
+ if col in ui_dict:
51
+ row[col] = ui_dict[col]
52
+
53
+ return pd.DataFrame([row], columns=expected_cols)
54
+
55
+
56
+ def predict_single(
57
+ payload: Dict[str, object],
58
+ internal_expected: List[str],
59
+ model,
60
+ feature_scaler,
61
+ target_scaler,
62
+ log_dir: Path,
63
+ model_path: Path,
64
+ schema: dict,
65
+ target_name: str,
66
+ encoder,
67
+ ) -> Tuple[float, str]:
68
+ """
69
+ Implémentation officielle :
70
+ UI → encodage Gender → scaling features → prédiction → inverse_transform cible.
71
+ """
72
+
73
+ # 0) Règle métier : si Workout_Type == "None" → prédiction forcée à 1 XP
74
+ workout_type_raw = payload.get("Workout_Type")
75
+ if isinstance(workout_type_raw, str) and workout_type_raw.strip().lower() == "none":
76
+ y_xp = 1.0
77
+ # Logging même si règle métier
78
+ log_prediction(
79
+ log_dir=log_dir,
80
+ row_in=payload,
81
+ y_hat=y_xp,
82
+ latency_ms=0,
83
+ model_filename=model_path.name,
84
+ model_version=schema.get("model_version", "unknown"),
85
+ target_name=target_name,
86
+ )
87
+
88
+ meta = (
89
+ f"Model: {model_path.name} | "
90
+ f"Version: {schema.get('model_version','?')} | "
91
+ f"Features: {', '.join(internal_expected)} | "
92
+ f"Rule applied: Workout_Type=None → y_xp=1"
93
+ )
94
+ return y_xp, meta
95
+
96
+ # 1) Construire le DF interne
97
+ X_raw = ui_to_internal_row(payload, internal_expected, encoder)
98
+
99
+ # 2) Scaling des features
100
+ X_scaled = pd.DataFrame(
101
+ feature_scaler.transform(X_raw),
102
+ columns=internal_expected,
103
+ index=X_raw.index,
104
+ )
105
+
106
+ # 3) Prédiction standardisée
107
+ y_std = float(model.predict(X_scaled)[0])
108
+
109
+ # 4) Remise en unités réelles
110
+ y_xp = float(target_scaler.inverse_transform(np.array([[y_std]]))[0, 0])
111
+ y_xp = round(y_xp, 2)
112
+
113
+ # 5) Logging
114
+ log_prediction(
115
+ log_dir=log_dir,
116
+ row_in=payload,
117
+ y_hat=y_xp,
118
+ latency_ms=0,
119
+ model_filename=model_path.name,
120
+ model_version=schema.get("model_version", "unknown"),
121
+ target_name=target_name,
122
+ )
123
+
124
+ meta = (
125
+ f"Model: {model_path.name} | "
126
+ f"Version: {schema.get('model_version','?')} | "
127
+ f"Features: {', '.join(internal_expected)}"
128
+ )
129
+ return y_xp, meta
130
+
src/gradio/helpers/preprocess_utils.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import pandas as pd
3
+
4
+ def maybe_apply_feature_scaler(X: pd.DataFrame, scaler):
5
+ """Applique scaler.transform(X) si présent, sinon renvoie X."""
6
+ if scaler is None:
7
+ return X
8
+ Xs = scaler.transform(X)
9
+ return pd.DataFrame(Xs, columns=X.columns, index=X.index)
10
+
11
+ def maybe_inverse_target(y_pred_float: float, y_scaler):
12
+ """
13
+ Si un y_scaler est fourni, applique inverse_transform.
14
+ Renvoie (y_pred_final: float, applied: bool).
15
+ """
16
+ if y_scaler is None:
17
+ return y_pred_float, False
18
+ try:
19
+ y_final = float(y_scaler.inverse_transform(np.array([[y_pred_float]])).ravel()[0])
20
+ return y_final, True
21
+ except Exception:
22
+ return y_pred_float, False
src/gradio/helpers/report_utils.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import pandas as pd
3
+ from pathlib import Path
4
+
5
+
6
+ def read_model_report(path: Path) -> dict:
7
+ """
8
+ Lit le fichier JSON du rapport de modèle.
9
+ Retourne un dictionnaire vide en cas d'erreur.
10
+ """
11
+ try:
12
+ with open(path, "r", encoding="utf-8") as f:
13
+ return json.load(f)
14
+ except Exception:
15
+ return {}
16
+
17
+
18
+ def report_summary_df(rep: dict) -> pd.DataFrame:
19
+ """
20
+ Construit un tableau récapitulatif des informations principales du rapport.
21
+ """
22
+ if not rep:
23
+ return pd.DataFrame({
24
+ "Info": [
25
+ "created_at", "target", "n_features", "n_test_samples",
26
+ "selected_model.type", "selected_model.class"
27
+ ],
28
+ "Valeur": ["-", "-", "-", "-", "-", "-"]
29
+ })
30
+
31
+ sel = rep.get("selected_model", {})
32
+ rows = [
33
+ ("created_at", rep.get("created_at", "-")),
34
+ ("target", rep.get("target", "-")),
35
+ ("n_features", rep.get("n_features", "-")),
36
+ ("n_test_samples", rep.get("n_test_samples", "-")),
37
+ ("selected_model.type", sel.get("model_type", "-")),
38
+ ("selected_model.class", sel.get("model_class", "-")),
39
+ ]
40
+ return pd.DataFrame(rows, columns=["Informations", "Value"])
41
+
42
+
43
+ def report_metrics_df(rep: dict) -> pd.DataFrame:
44
+ """
45
+ Construit un tableau des métriques de performance par modèle.
46
+ Colonnes : Modèle, MAE, RMSE, R2.
47
+ """
48
+ mets = rep.get("metrics_by_model", {})
49
+ if not mets:
50
+ return pd.DataFrame(columns=["Model", "MAE", "RMSE", "R2"])
51
+
52
+ df = pd.DataFrame(mets).T.reset_index().rename(columns={"index": "Model"})
53
+
54
+ # Conversion et arrondis
55
+ for c in ("MAE", "RMSE", "R2"):
56
+ if c in df.columns:
57
+ df[c] = pd.to_numeric(df[c], errors="coerce").round(3)
58
+
59
+ return df[["Model", "MAE", "RMSE", "R2"]]
src/gradio/helpers/schema_utils.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Tuple, Any
2
+ from ..config import UI_DEFAULTS # si tu as ajouté le dict ci-dessus
3
+
4
+ def _coerce_num(x, is_int: bool):
5
+ try:
6
+ v = float(x)
7
+ return int(v) if is_int else float(v)
8
+ except Exception:
9
+ return None
10
+
11
+ def get_bounds(spec: dict, schema: dict) -> Tuple[float, float, Any, float]:
12
+ """
13
+ Calcule (min, max, default, step) pour un champ.
14
+ Si l'exemple du schéma est manquant/hors bornes, on tombe sur le milieu [min,max].
15
+ On autorise un override via UI_DEFAULTS.
16
+ """
17
+ is_int = spec.get("type", "number") in ("integer", "int")
18
+
19
+ # bornes
20
+ vmin = int(spec.get("min", 0)) if is_int else float(spec.get("min", 0.0))
21
+ vmax = int(spec.get("max", 100)) if is_int else float(spec.get("max", 100.0))
22
+
23
+ # fallback = milieu propre
24
+ fallback = (vmin + vmax) // 2 if is_int else (vmin + vmax) / 2
25
+
26
+ # 1) tentative depuis UI_DEFAULTS
27
+ dflt = UI_DEFAULTS.get(spec["name"])
28
+ dflt = _coerce_num(dflt, is_int)
29
+
30
+ # 2) sinon, tentative depuis example_payload
31
+ if dflt is None:
32
+ ex = schema.get("example_payload", {}).get(spec["name"])
33
+ dflt = _coerce_num(ex, is_int)
34
+
35
+ # 3) clamp/validate
36
+ if dflt is None or not (vmin <= dflt <= vmax):
37
+ dflt = fallback
38
+
39
+ # step
40
+ step = 1 if is_int else (0.1 if (vmax - vmin) <= 20 else 0.5)
41
+
42
+ return vmin, vmax, dflt, step
src/gradio/helpers/sqlite_utils.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ import pandas as pd
3
+ from pathlib import Path
4
+ from typing import List
5
+
6
+
7
+ def list_tables(conn: sqlite3.Connection) -> List[str]:
8
+ """
9
+ Retourne la liste des tables non système présentes dans la base SQLite.
10
+ """
11
+ cur = conn.execute(
12
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
13
+ )
14
+ return [r[0] for r in cur.fetchall()]
15
+
16
+
17
+ def table_has_all_columns(conn: sqlite3.Connection, table: str, wanted: list[str]) -> bool:
18
+ """
19
+ Vérifie qu'une table contient toutes les colonnes demandées.
20
+ """
21
+ cur = conn.execute(f"PRAGMA table_info('{table}')")
22
+ cols = {row[1] for row in cur.fetchall()} # row[1] = nom de la colonne
23
+ return set(wanted).issubset(cols)
24
+
25
+
26
+ def load_val_subset(
27
+ db_path: Path,
28
+ wanted_cols: list[str],
29
+ target_name: str,
30
+ limit: int = 500,
31
+ ) -> pd.DataFrame:
32
+ """
33
+ Charge un sous-ensemble de données de validation depuis une base SQLite.
34
+ La cible (target_name) est toujours affichée en première colonne.
35
+ Si certaines colonnes manquent dans la table, elles sont ajoutées vides.
36
+
37
+ Args:
38
+ db_path: chemin vers la base SQLite.
39
+ wanted_cols: liste des colonnes de features attendues.
40
+ target_name: nom de la variable cible.
41
+ limit: nombre maximum de lignes à charger.
42
+
43
+ Returns:
44
+ DataFrame contenant les colonnes [target_name] + features.
45
+ """
46
+ display_cols = [target_name] + [c for c in wanted_cols if c != target_name]
47
+
48
+ if not db_path.exists():
49
+ return pd.DataFrame(columns=display_cols)
50
+
51
+ with sqlite3.connect(db_path) as conn:
52
+ # 1) Table contenant toutes les colonnes demandées
53
+ for tbl in list_tables(conn):
54
+ if table_has_all_columns(conn, tbl, display_cols):
55
+ cols_str = ", ".join([f'"{c}"' for c in display_cols])
56
+ query = f'SELECT {cols_str} FROM "{tbl}" LIMIT {limit}'
57
+ return pd.read_sql_query(query, conn)
58
+
59
+ # 2) Table partielle la plus proche (max d’intersection)
60
+ best_tbl, best_cols = None, []
61
+ for tbl in list_tables(conn):
62
+ cur = conn.execute(f"PRAGMA table_info('{tbl}')")
63
+ cols = {row[1] for row in cur.fetchall()}
64
+ inter = [c for c in display_cols if c in cols]
65
+ if len(inter) > len(best_cols):
66
+ best_tbl, best_cols = tbl, inter
67
+
68
+ if best_tbl:
69
+ cols_str = ", ".join([f'"{c}"' for c in best_cols])
70
+ query = f'SELECT {cols_str} FROM "{best_tbl}" LIMIT {limit}'
71
+ df = pd.read_sql_query(query, conn)
72
+
73
+ # Complète les colonnes manquantes (target/features) et garde l'ordre
74
+ for c in display_cols:
75
+ if c not in df.columns:
76
+ df[c] = pd.NA
77
+ return df[display_cols]
78
+
79
+ # 3) Aucun tableau exploitable
80
+ return pd.DataFrame(columns=display_cols)
src/gradio/model_loader.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import joblib
3
+ from pathlib import Path
4
+ from sklearn.pipeline import Pipeline
5
+ from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
6
+
7
+ def load_model_and_schema(model_path: Path, schema_path: Path):
8
+ """
9
+ Charge le modèle .joblib et le schéma JSON associé.
10
+ Retourne (model, schema, target_name, features, expected_order).
11
+ """
12
+ # --- Chargement du modèle ---
13
+ model = joblib.load(model_path)
14
+
15
+ # --- Chargement du schéma ---
16
+ with open(schema_path, "r", encoding="utf-8") as f:
17
+ schema = json.load(f)
18
+
19
+ # --- Extraction des infos principales ---
20
+ target_name = schema.get("target", "Experience_level")
21
+ features = schema.get("features", [])
22
+
23
+ # --- Ordre des colonnes attendu par le modèle ---
24
+ if hasattr(model, "feature_names_in_"):
25
+ expected_order = list(model.feature_names_in_)
26
+ else:
27
+ expected_order = [f["name"] for f in features]
28
+
29
+ return model, schema, target_name, features, expected_order
30
+
31
+ def load_optional_joblib(path: Path):
32
+ try:
33
+ if path and Path(path).exists():
34
+ return joblib.load(path)
35
+ except Exception:
36
+ pass
37
+ return None
38
+
39
+ def pipeline_has_scaler(p) -> bool:
40
+ """
41
+ True si 'p' est un Pipeline sklearn qui contient un scaler connu.
42
+ """
43
+ if not isinstance(p, Pipeline):
44
+ return False
45
+ scaler_types = (StandardScaler, MinMaxScaler, RobustScaler)
46
+ return any(isinstance(step, scaler_types) for _, step in p.named_steps.items())
src/gradio/pages/dl_execution_tab.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Union
2
+ from pathlib import Path
3
+
4
+ import gradio as gr
5
+ import pandas as pd
6
+
7
+ from ..helpers.exercices_tab_utilis import (
8
+ DEFAULT_EXERCICES_PATH,
9
+ _load_exercices,
10
+ )
11
+ from ..generators.execution_generator import (
12
+ build_execution_prompt,
13
+ generate_execution_text,
14
+ get_dl_execution_model_report_components,
15
+ )
16
+
17
+
18
+ def render_dl_execution_tab(
19
+ app_desc_dl_exec: str,
20
+ dataset_path: Union[str, Path] = DEFAULT_EXERCICES_PATH,
21
+ ):
22
+ """
23
+ Onglet 'Deep Learning - Execution generator' :
24
+ sélection d'un programme SANS execution pour générer son texte d'exécution.
25
+ """
26
+
27
+ df = _load_exercices(dataset_path)
28
+
29
+ # ---- Filtrer uniquement les programmes sans execution ----
30
+ if "execution" in df.columns:
31
+ mask_no_exec = df["execution"].isna() | (df["execution"].astype(str).str.strip() == "")
32
+ df_no_exec = df[mask_no_exec].copy()
33
+ else:
34
+ df_no_exec = df.copy()
35
+
36
+ has_name_col = "exercise_name" in df_no_exec.columns
37
+ exercice_choices = (
38
+ sorted(df_no_exec["exercise_name"].dropna().unique().tolist())
39
+ if has_name_col
40
+ else []
41
+ )
42
+
43
+ # Colonnes affichées dans le tableau, y compris execution pour vérification
44
+ base_cols = ["exercise_name", "target_muscles", "equipment", "difficulty", "execution"]
45
+ selected_cols = [c for c in base_cols if c in df_no_exec.columns]
46
+
47
+ with gr.Tab("Deep Learning - Execution generator") as tab_dl_exec:
48
+ gr.Markdown(f"## {app_desc_dl_exec} - V3")
49
+
50
+ gr.Markdown("### Program details")
51
+
52
+ with gr.Row():
53
+ exercice_selector = gr.Dropdown(
54
+ label="Select a program without execution",
55
+ choices=exercice_choices,
56
+ value=exercice_choices[0] if exercice_choices else None,
57
+ )
58
+
59
+ details_md = gr.Markdown(
60
+ value=(
61
+ "Select a program **without execution description** "
62
+ "to see its details."
63
+ ),
64
+ )
65
+
66
+ # Tableau : programme sélectionné (avec colonne execution pour contrôle)
67
+ selected_program_exec_df = gr.Dataframe(
68
+ value=pd.DataFrame(columns=selected_cols),
69
+ interactive=False,
70
+ wrap=True,
71
+ label="Selected program",
72
+ row_count=(0, "dynamic"),
73
+ col_count=(0, "dynamic"),
74
+ )
75
+
76
+ # Prompt auto-généré
77
+ prompt_box = gr.Textbox(
78
+ label="Execution prompt (auto-generated)",
79
+ interactive=False,
80
+ lines=3,
81
+ max_lines=5,
82
+ )
83
+
84
+ # Bouton + sortie génération
85
+ generate_btn = gr.Button("Generate execution")
86
+
87
+ generated_exec = gr.Textbox(
88
+ label="Generated execution",
89
+ lines=10,
90
+ max_lines=20,
91
+ )
92
+
93
+ gr.Markdown("### Execution generator – Model report")
94
+
95
+ dl_summary = gr.Dataframe(
96
+ value=pd.DataFrame({"Key": [], "Value": []}),
97
+ interactive=False,
98
+ wrap=True,
99
+ label="Summary",
100
+ )
101
+
102
+ dl_model = gr.Dataframe(
103
+ value=pd.DataFrame({"Key": [], "Value": []}),
104
+ interactive=False,
105
+ wrap=True,
106
+ label="Model",
107
+ )
108
+
109
+ dl_training = gr.Dataframe(
110
+ value=pd.DataFrame({"Key": [], "Value": []}),
111
+ interactive=False,
112
+ wrap=True,
113
+ label="Training",
114
+ )
115
+
116
+ dl_metrics = gr.Dataframe(
117
+ value=pd.DataFrame({"Metric": [], "Value": []}),
118
+ interactive=False,
119
+ wrap=True,
120
+ label="Metrics",
121
+ )
122
+
123
+ # Callback de mise à jour details + tableau + prompt
124
+ def _format_details_exec(ex_name: str):
125
+ empty_df = pd.DataFrame(columns=selected_cols)
126
+ empty_prompt = ""
127
+
128
+ if not ex_name:
129
+ return (
130
+ "Select a program **without execution description** "
131
+ "to see its details.",
132
+ empty_df,
133
+ empty_prompt,
134
+ )
135
+
136
+ subset = df_no_exec[df_no_exec["exercise_name"] == ex_name]
137
+ if subset.empty:
138
+ return "No details found for this program.", empty_df, empty_prompt
139
+
140
+ row = subset.iloc[0]
141
+
142
+ def get(col, default="—"):
143
+ return row[col] if col in row and pd.notna(row[col]) else default
144
+
145
+ parts = [
146
+ f"**Name** : {get('exercise_name')}",
147
+ f"**Target muscles** : {get('target_muscles')}",
148
+ f"**Equipment** : {get('equipment')}",
149
+ f"**Difficulty** : {get('difficulty')}",
150
+ "",
151
+ "This program currently has **no execution description**.",
152
+ "You can generate a detailed execution using the Deep Learning model.",
153
+ ]
154
+ details_text = "\n".join(parts)
155
+
156
+ sel_row = {c: get(c, "") for c in selected_cols}
157
+ sel_df = pd.DataFrame([sel_row])
158
+
159
+ # Prompt pour ce programme
160
+ prompt = build_execution_prompt(row)
161
+
162
+ return details_text, sel_df, prompt
163
+
164
+ def _update_exec_report():
165
+ df_summary, df_model_df, df_training_df, df_metrics_df = get_dl_execution_model_report_components()
166
+ return df_summary, df_model_df, df_training_df, df_metrics_df
167
+
168
+
169
+
170
+ exercice_selector.change(
171
+ _format_details_exec,
172
+ inputs=exercice_selector,
173
+ outputs=[details_md, selected_program_exec_df, prompt_box],
174
+ )
175
+
176
+ # Génération à partir du prompt construit
177
+ generate_btn.click(
178
+ fn=generate_execution_text,
179
+ inputs=prompt_box,
180
+ outputs=generated_exec,
181
+ )
182
+
183
+ tab_dl_exec.select(
184
+ _update_exec_report,
185
+ inputs=None, # ou [] mais None évite le warning
186
+ outputs=[dl_summary, dl_model, dl_training, dl_metrics],
187
+ )
188
+
189
+ # On retourne le DF sélectionné pour l’execution generator
190
+ return selected_program_exec_df
src/gradio/pages/dl_tab.py ADDED
@@ -0,0 +1,278 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # import gradio as gr
2
+ # import pandas as pd
3
+
4
+ # # Import du helper
5
+ # from ..helpers.model_manager import (
6
+ # on_model_change,
7
+ # generate_text_with_model,
8
+ # get_dl_model_report_components,
9
+ # )
10
+
11
+
12
+ # def render_dl_tab(
13
+ # app_desc_dl: str,
14
+ # level_out: gr.Textbox,
15
+ # wf_comp: gr.Component,
16
+ # wt_comp: gr.Component,
17
+ # selected_program_df: gr.State,
18
+ # goal_state: gr.State,
19
+ # ) -> dict:
20
+ # """Onglet Deep Learning (sélection du modèle + prompt + sortie texte)."""
21
+
22
+ # with gr.Tab(f"Deep Learning - {app_desc_dl}") as tab_dl:
23
+
24
+ # # ===== Rappel du profil utilisateur (venant du tab ML) =====
25
+ # gr.Markdown("### Your profile (from Machine Learning tab)")
26
+
27
+ # with gr.Row():
28
+ # level_display = gr.Textbox(
29
+ # label="Physical level",
30
+ # interactive=False,
31
+ # lines=1,
32
+ # max_lines=1,
33
+ # )
34
+ # wf_display = gr.Textbox(
35
+ # label="Workout frequency (days/week)",
36
+ # interactive=False,
37
+ # lines=1,
38
+ # max_lines=1,
39
+ # )
40
+ # wt_display = gr.Textbox(
41
+ # label="Workout type",
42
+ # interactive=False,
43
+ # lines=1,
44
+ # max_lines=1,
45
+ # )
46
+
47
+ # gr.Markdown("### Selected program (from List of programs)")
48
+
49
+ # goal_display = gr.Textbox(
50
+ # label="Training goal",
51
+ # interactive=False,
52
+ # lines=1,
53
+ # max_lines=1,
54
+ # )
55
+
56
+ # program_display = gr.Dataframe(
57
+ # value=pd.DataFrame(),
58
+ # interactive=False,
59
+ # wrap=True,
60
+ # label="Selected program",
61
+ # row_count=(0, "dynamic"),
62
+ # col_count=(0, "dynamic"),
63
+ # )
64
+
65
+ # gr.Markdown(
66
+ # "These values are synchronized with your Machine Learning profile "
67
+ # "and are used to build the Deep Learning prompt automatically."
68
+ # )
69
+
70
+ # gr.Markdown(
71
+ # "## Generate your personalized exercise\n"
72
+ # "Prediction based on your information and the program selected from the list"
73
+ # )
74
+
75
+ # # Champ Prompt (auto-généré)
76
+ # prompt_box = gr.Textbox(
77
+ # label="Prompt sent to the Deep Learning model",
78
+ # interactive=True,
79
+ # lines=4,
80
+ # )
81
+
82
+ # # Sélecteur du modèle DL
83
+ # model_selector = gr.Dropdown(
84
+ # label="Deep Learning model",
85
+ # choices=[
86
+ # "xMas - GPT2 Fine-tuning",
87
+ # ],
88
+ # value="xMas - GPT2 Fine-tuning",
89
+ # )
90
+
91
+ # # Zone d'info sur le modèle chargé
92
+ # model_status = gr.Markdown(
93
+ # "No model loaded yet. Select one from the list above.",
94
+ # )
95
+
96
+ # gr.Markdown("---")
97
+
98
+ # # Bouton "Générer" centré
99
+ # with gr.Row():
100
+ # gr.Column(scale=1)
101
+ # with gr.Column(scale=1):
102
+ # generate_btn = gr.Button("Générer")
103
+ # gr.Column(scale=1)
104
+
105
+ # # Zone d’affichage du texte généré (programme)
106
+ # generated_text = gr.Textbox(
107
+ # label="Generated program",
108
+ # lines=20,
109
+ # max_lines=40,
110
+ # )
111
+
112
+ # # Wiring : clic sur "Générer"
113
+ # generate_btn.click(
114
+ # fn=generate_text_with_model,
115
+ # inputs=[model_selector, prompt_box],
116
+ # outputs=generated_text,
117
+ # )
118
+
119
+ # # Tableaux du rapport DL
120
+ # gr.Markdown("### Deep Learning model evaluation report")
121
+
122
+ # dl_sum = gr.Dataframe(
123
+ # value=pd.DataFrame({"Key": [], "Value": []}),
124
+ # interactive=False,
125
+ # wrap=True,
126
+ # label="Summary",
127
+ # )
128
+
129
+ # dl_model = gr.Dataframe(
130
+ # value=pd.DataFrame({"Key": [], "Value": []}),
131
+ # interactive=False,
132
+ # wrap=True,
133
+ # label="Model",
134
+ # )
135
+
136
+ # dl_training = gr.Dataframe(
137
+ # value=pd.DataFrame({"Key": [], "Value": []}),
138
+ # interactive=False,
139
+ # wrap=True,
140
+ # label="Training",
141
+ # )
142
+
143
+ # dl_metrics = gr.Dataframe(
144
+ # value=pd.DataFrame({"Metric": [], "Value": []}),
145
+ # interactive=False,
146
+ # wrap=True,
147
+ # label="Metrics",
148
+ # )
149
+
150
+ # # --- Helpers internes ---
151
+
152
+ # def _build_prompt(
153
+ # level_text: str,
154
+ # wf_text: str,
155
+ # wt_text: str,
156
+ # program_row,
157
+ # goal_text: str,
158
+ # ) -> str:
159
+ # """
160
+ # Construit le prompt à partir du profil + programme sélectionné.
161
+ # """
162
+ # level = level_text or "Unknown"
163
+ # wf = wf_text or "N/A"
164
+ # wt = wt_text or "Olympic Weightlifting"
165
+ # goal = goal_text or "Olympic Weightlifting"
166
+
167
+ # if not program_row:
168
+ # return (
169
+ # "Generate a workout program based on the user's physical level "
170
+ # "and training goal. No specific exercise has been selected yet."
171
+ # )
172
+
173
+ # ex_name = program_row.get("exercise_name") or "the selected exercise"
174
+ # target = program_row.get("target_muscles") or "the target muscles"
175
+ # equip = program_row.get("equipment") or "bodyweight only"
176
+
177
+ # prompt = (
178
+ # f"Generate a workout program at [{level}] level. "
179
+ # f"I am currently training for [{wt}] and my training frequency "
180
+ # f"is [{wf}] days per week. "
181
+ # f"My main goal is [{goal}]. "
182
+ # f"The exercise to generate is titled [{ex_name}], "
183
+ # f"it targets the [{target}] and uses the following equipment: "
184
+ # f"[{equip}]."
185
+ # )
186
+ # return prompt
187
+
188
+ # # --- Callbacks ---
189
+
190
+ # def _on_dl_model_select(name: str):
191
+ # status = on_model_change(name)
192
+ # df_sum, df_model, df_training, df_metrics = get_dl_model_report_components(
193
+ # name
194
+ # )
195
+ # return status, df_sum, df_model, df_training, df_metrics
196
+
197
+ # # Quand on change de modèle manuellement
198
+ # model_selector.change(
199
+ # fn=_on_dl_model_select,
200
+ # inputs=model_selector,
201
+ # outputs=[model_status, dl_sum, dl_model, dl_training, dl_metrics],
202
+ # )
203
+
204
+ # # Synchronisation du profil + programme + prompt + rapport à l'ouverture du tab DL
205
+ # def _sync_profile(level_val, wf_val, wt_val, program_row, goal_val, model_name):
206
+ # level_text = level_val or ""
207
+ # wf_text = "" if wf_val in (None, "") else str(wf_val)
208
+ # wt_text = wt_val or ""
209
+ # goal_text = goal_val or "Olympic Weightlifting"
210
+
211
+ # if not program_row:
212
+ # program_df = pd.DataFrame()
213
+ # else:
214
+ # program_df = pd.DataFrame([program_row])
215
+
216
+ # prompt = _build_prompt(level_text, wf_text, wt_text, program_row, goal_text)
217
+
218
+ # # Chargement du modèle + rapport dès ouverture du tab
219
+ # status = on_model_change(model_name)
220
+ # df_sum, df_model, df_training, df_metrics = get_dl_model_report_components(
221
+ # model_name
222
+ # )
223
+
224
+ # return (
225
+ # level_text,
226
+ # wf_text,
227
+ # wt_text,
228
+ # goal_text,
229
+ # program_df,
230
+ # prompt,
231
+ # status,
232
+ # df_sum,
233
+ # df_model,
234
+ # df_training,
235
+ # df_metrics,
236
+ # )
237
+
238
+ # tab_dl.select(
239
+ # _sync_profile,
240
+ # inputs=[
241
+ # level_out,
242
+ # wf_comp,
243
+ # wt_comp,
244
+ # selected_program_df,
245
+ # goal_state,
246
+ # model_selector,
247
+ # ],
248
+ # outputs=[
249
+ # level_display,
250
+ # wf_display,
251
+ # wt_display,
252
+ # goal_display,
253
+ # program_display,
254
+ # prompt_box,
255
+ # model_status,
256
+ # dl_sum,
257
+ # dl_model,
258
+ # dl_training,
259
+ # dl_metrics,
260
+ # ],
261
+ # )
262
+
263
+ # # Retour des composants si besoin
264
+ # return {
265
+ # "model_selector": model_selector,
266
+ # "model_status": model_status,
267
+ # "prompt_box": prompt_box,
268
+ # "generated_text": generated_text,
269
+ # "level_display": level_display,
270
+ # "wf_display": wf_display,
271
+ # "wt_display": wt_display,
272
+ # "goal_display": goal_display,
273
+ # "program_display": program_display,
274
+ # "dl_sum": dl_sum,
275
+ # "dl_model": dl_model,
276
+ # "dl_training": dl_training,
277
+ # "dl_metrics": dl_metrics,
278
+ # }
src/gradio/pages/exercices_tab.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # from typing import Union
2
+ # from pathlib import Path
3
+
4
+ # import gradio as gr
5
+ # import pandas as pd
6
+
7
+ # from ..helpers.exercices_tab_utilis import (
8
+ # DEFAULT_EXERCICES_PATH,
9
+ # DEFAULT_GOAL_PATH,
10
+ # _filter_by_level,
11
+ # _load_exercices,
12
+ # _load_goals,
13
+ # )
14
+
15
+
16
+ # def render_list_of_exercices(
17
+ # app_desc_ex: str,
18
+ # level_out: gr.Textbox, # textbox du ML tab
19
+ # dataset_path: Union[str, Path] = DEFAULT_EXERCICES_PATH,
20
+ # goal_path: Union[str, Path] = DEFAULT_GOAL_PATH,
21
+ # ):
22
+ # """
23
+ # Onglet 'Exercices proposés' : tableau + panneau de détails.
24
+ # """
25
+
26
+ # df = _load_exercices(dataset_path)
27
+
28
+ # # --- Vue "compacte" pour le tableau ---
29
+ # df_view = df.copy()
30
+ # if "execution" in df_view.columns:
31
+ # df_view["Execution (preview)"] = (
32
+ # df_view["execution"].astype(str).str.slice(0, 80).fillna("") + "..."
33
+ # )
34
+
35
+ # base_cols = [
36
+ # "exercise_name",
37
+ # "target_muscles",
38
+ # "equipment",
39
+ # "difficulty",
40
+ # ]
41
+
42
+ # cols = [c for c in base_cols if c in df_view.columns]
43
+ # cols.append("Execution (preview)")
44
+ # df_view = df_view[cols]
45
+
46
+ # # Colonnes à transférer vers le tab DL
47
+ # selected_cols = [
48
+ # c
49
+ # for c in [
50
+ # "exercise_name",
51
+ # "target_muscles",
52
+ # "equipment",
53
+ # "difficulty",
54
+ # "execution",
55
+ # ]
56
+ # if c in df.columns
57
+ # ]
58
+ # else:
59
+ # selected_cols = []
60
+
61
+ # # Liste pour le panneau de détails
62
+ # has_name_col = "exercise_name" in df.columns
63
+ # exercice_choices = (
64
+ # sorted(df["exercise_name"].dropna().unique().tolist())
65
+ # if has_name_col
66
+ # else []
67
+ # )
68
+
69
+ # # --- Chargement des goals ---
70
+ # try:
71
+ # goals = _load_goals(goal_path)
72
+ # except Exception:
73
+ # goals = []
74
+
75
+ # if not goals:
76
+ # goals = ["General Fitness"]
77
+
78
+ # default_goal = "General Fitness" if "General Fitness" in goals else goals[0]
79
+
80
+
81
+ # with gr.Tab("List of programs") as tab_ex:
82
+ # # Sélection du goal
83
+ # gr.Markdown("## Select your goal")
84
+
85
+ # goal_dropdown = gr.Dropdown(
86
+ # label="List of goals",
87
+ # choices=goals,
88
+ # value=default_goal,
89
+ # interactive=True,
90
+ # )
91
+ # goal_state = gr.State(value=None)
92
+
93
+ # gr.Markdown(f"## {app_desc_ex}")
94
+
95
+ # # Niveau ML
96
+ # with gr.Row():
97
+ # level_display = gr.Textbox(
98
+ # label="Physical level",
99
+ # interactive=False,
100
+ # lines=1,
101
+ # max_lines=1,
102
+ # )
103
+
104
+ # # Recherche
105
+ # search_box = gr.Textbox(
106
+ # label="Search in table",
107
+ # placeholder="Name, muscles, equipment, difficulty…",
108
+ # )
109
+
110
+ # # Dropdown muscles — rempli dynamiquement
111
+ # muscle_filter = gr.Dropdown(
112
+ # label="Filter by target muscles",
113
+ # choices=["All"],
114
+ # value="All",
115
+ # )
116
+
117
+ # # Dropdown equipment — rempli dynamiquement
118
+ # equipment_filter = gr.Dropdown(
119
+ # label="Filter by equipment",
120
+ # choices=["All"],
121
+ # value="All",
122
+ # )
123
+
124
+ # gr.Markdown(
125
+ # "The table below shows an overview of each exercise.\n\n"
126
+ # "- ML level filters difficulty\n"
127
+ # "- Use search, target muscles and equipment filters to refine\n"
128
+ # "- Select a program below to see full execution details\n"
129
+ # )
130
+
131
+ # # --- Tableau ---
132
+ # table = gr.Dataframe(
133
+ # value=df_view,
134
+ # interactive=False,
135
+ # wrap=True,
136
+ # row_count=(0, "dynamic"),
137
+ # col_count=(0, "dynamic"),
138
+ # )
139
+
140
+ # # --- Panneau de détails + sélection ---
141
+ # selected_program_state = gr.State(value=None) # 👈 pour le tab DL
142
+
143
+ # if has_name_col:
144
+ # gr.Markdown("### Program details")
145
+
146
+ # with gr.Row():
147
+ # exercice_selector = gr.Dropdown(
148
+ # label="Select a program",
149
+ # choices=exercice_choices,
150
+ # value=exercice_choices[0] if exercice_choices else None,
151
+ # )
152
+
153
+ # details_md = gr.Markdown(
154
+ # value="Select a program to see full description.",
155
+ # )
156
+
157
+ # # Tableau 1 ligne : programme sélectionné (affiché ici)
158
+ # selected_program_df = gr.Dataframe(
159
+ # value=pd.DataFrame(columns=selected_cols),
160
+ # interactive=False,
161
+ # wrap=True,
162
+ # label="Selected program (for DL tab)",
163
+ # row_count=(0, "dynamic"),
164
+ # col_count=(0, "dynamic"),
165
+ # )
166
+
167
+ # def _format_details(ex_name: str):
168
+ # # DF vide par défaut
169
+ # empty_df = pd.DataFrame(columns=selected_cols)
170
+
171
+ # if not ex_name:
172
+ # return (
173
+ # "Select a program to see full description.",
174
+ # empty_df,
175
+ # None,
176
+ # )
177
+
178
+ # subset = df[df["exercise_name"] == ex_name]
179
+ # if subset.empty:
180
+ # return "No details found for this program.", empty_df, None
181
+
182
+ # row = subset.iloc[0]
183
+
184
+ # def get(col, default="—"):
185
+ # return row[col] if col in row and pd.notna(row[col]) else default
186
+
187
+ # parts = [
188
+ # f"**Name** : {get('exercise_name')}",
189
+ # f"**Target muscles** : {get('target_muscles')}",
190
+ # f"**Equipment** : {get('equipment')}",
191
+ # f"**Difficulty** : {get('difficulty')}",
192
+ # "",
193
+ # ]
194
+
195
+ # exec_text = get("execution", "")
196
+ # if exec_text and exec_text != "—":
197
+ # parts.append("**Execution** :")
198
+ # parts.append("")
199
+ # parts.append(exec_text)
200
+
201
+ # details_text = "\n".join(parts)
202
+
203
+ # # DF 1 ligne pour affichage
204
+ # sel_row = {c: get(c, "") for c in selected_cols}
205
+ # sel_df = pd.DataFrame([sel_row])
206
+
207
+ # # Dict pour le tab DL
208
+ # sel_dict = {c: get(c, "") for c in selected_cols}
209
+
210
+ # return details_text, sel_df, sel_dict
211
+
212
+ # exercice_selector.change(
213
+ # _format_details,
214
+ # inputs=exercice_selector,
215
+ # outputs=[details_md, selected_program_df, selected_program_state],
216
+ # )
217
+
218
+ # # ===== Callbacks =====
219
+
220
+ # # Synchronisation + filtrage niveau à l'ouverture de l'onglet
221
+ # def _sync_on_tab_open(level_val: str):
222
+ # level_text = level_val or ""
223
+ # filtered = _filter_by_level(df_view, level_text)
224
+
225
+ # # Muscles dynamiques selon niveau ML
226
+ # if "target_muscles" in filtered.columns:
227
+ # muscles = sorted(set(filtered["target_muscles"].dropna()))
228
+ # else:
229
+ # muscles = []
230
+
231
+ # # Equipment dynamique selon niveau ML
232
+ # if "equipment" in filtered.columns:
233
+ # equipments = sorted(set(filtered["equipment"].dropna()))
234
+ # else:
235
+ # equipments = []
236
+
237
+ # return (
238
+ # level_text,
239
+ # filtered,
240
+ # gr.update(choices=["All"] + muscles, value="All"),
241
+ # gr.update(choices=["All"] + equipments, value="All"),
242
+ # )
243
+
244
+ # tab_ex.select(
245
+ # _sync_on_tab_open,
246
+ # inputs=[level_out],
247
+ # outputs=[level_display, table, muscle_filter, equipment_filter],
248
+ # )
249
+
250
+ # # Recherche texte + filtres muscle & equipment
251
+ # def _search_table(
252
+ # query: str,
253
+ # level_val: str,
254
+ # muscle_choice: str,
255
+ # equipment_choice: str,
256
+ # ):
257
+ # # Filtre niveau ML
258
+ # base = _filter_by_level(df_view, level_val or "")
259
+
260
+ # # Filtre muscles
261
+ # if muscle_choice != "All" and "target_muscles" in base.columns:
262
+ # base = base[base["target_muscles"] == muscle_choice]
263
+
264
+ # # Filtre equipment
265
+ # if equipment_choice != "All" and "equipment" in base.columns:
266
+ # base = base[base["equipment"] == equipment_choice]
267
+
268
+ # # Recherche
269
+ # if query:
270
+ # df_str = base.astype(str)
271
+ # mask = df_str.apply(
272
+ # lambda row: row.str.contains(query, case=False, na=False).any(),
273
+ # axis=1,
274
+ # )
275
+ # base = base[mask]
276
+
277
+ # return base
278
+
279
+ # def _on_goal_change(goal_val):
280
+ # return goal_val
281
+
282
+ # goal_dropdown.change(
283
+ # _on_goal_change,
284
+ # inputs=goal_dropdown,
285
+ # outputs=goal_state,
286
+ # )
287
+
288
+ # search_box.change(
289
+ # _search_table,
290
+ # inputs=[search_box, level_out, muscle_filter, equipment_filter],
291
+ # outputs=table,
292
+ # )
293
+
294
+ # muscle_filter.change(
295
+ # _search_table,
296
+ # inputs=[search_box, level_out, muscle_filter, equipment_filter],
297
+ # outputs=table,
298
+ # )
299
+
300
+ # equipment_filter.change(
301
+ # _search_table,
302
+ # inputs=[search_box, level_out, muscle_filter, equipment_filter],
303
+ # outputs=table,
304
+ # )
305
+
306
+ # # On retourne l'état (dict) du programme sélectionné
307
+ # return selected_program_state, goal_state
308
+
src/gradio/pages/ml_tab.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import gradio as gr
3
+ import pandas as pd
4
+
5
+ from ..helpers.schema_utils import get_bounds
6
+ from ..helpers.sqlite_utils import load_val_subset
7
+ from ..helpers.predict_utils import predict_single
8
+ from ..helpers.report_utils import (
9
+ read_model_report, report_summary_df, report_metrics_df
10
+ )
11
+ from typing import Dict, List
12
+
13
+
14
+ def render_ml_tab(
15
+ app_desc_ml: str,
16
+ feature_specs: List[dict],
17
+ ui_feature_names: List[str],
18
+ internal_expected: List[str],
19
+ target_name: str,
20
+ schema: dict,
21
+ ui_examples: List[Dict],
22
+ db_path: Path,
23
+ model,
24
+ logs_dir: Path,
25
+ model_path: Path,
26
+ feature_scaler,
27
+ target_scaler,
28
+ encoder,
29
+ report_path: Path,
30
+ on_load=None,
31
+ ) -> None:
32
+ display_headers = [target_name] + [c for c in ui_feature_names if c != target_name]
33
+
34
+ with gr.Tab(f"Machine Learning - {app_desc_ml} ({schema.get('model_name','model')})"):
35
+ # ====== Ligne principale (inputs/pred) ======
36
+ with gr.Row():
37
+ with gr.Column():
38
+ gr.Markdown("### Assess your physical level")
39
+ comps, names = [], []
40
+
41
+ wf_comp = None # Workout_Frequency (days/week)
42
+ wt_comp = None # Workout_Type
43
+
44
+ for spec in feature_specs:
45
+ name = spec["name"]
46
+ if name == "Experience_Level":
47
+ comp = gr.Slider(0, 3, value=0, step=0, label=name, visible=False)
48
+
49
+ comps.append(comp)
50
+ names.append(name)
51
+ continue
52
+
53
+ if name == "Gender":
54
+ # UI = Radio pour Male / Female
55
+ choices = spec.get("enum", ["Male", "Female"])
56
+ comp = gr.Radio(
57
+ choices=choices,
58
+ value=choices[0],
59
+ label="Gender",
60
+ )
61
+
62
+ elif name == "Workout_Type":
63
+ # UI = Liste déroulante pour le type de séance
64
+ choices = spec.get("enum", ["Cardio", "Strength", "HIIT", "Yoga"])
65
+ comp = gr.Dropdown(
66
+ choices=choices,
67
+ value=choices[0],
68
+ label="Workout_Type",
69
+ )
70
+ wt_comp = comp # on garde une référence
71
+
72
+ else:
73
+ vmin, vmax, default, step = get_bounds(spec, schema)
74
+ comp = gr.Slider(
75
+ vmin,
76
+ vmax,
77
+ value=default,
78
+ step=step,
79
+ label=name,
80
+ )
81
+
82
+ if name == "Workout_Frequency (days/week)":
83
+ wf_comp = comp # on garde une référence
84
+
85
+ comps.append(comp)
86
+ names.append(name)
87
+
88
+ btn = gr.Button("Predict", variant="primary")
89
+
90
+ with gr.Column():
91
+ gr.Markdown("### Calculs")
92
+
93
+ bmi_out = gr.Number(
94
+ label="BMI (Body Mass Index)",
95
+ interactive=False,
96
+ precision=2,
97
+ )
98
+ fat_out = gr.Number(
99
+ label="Body fat percentage (%)",
100
+ interactive=False,
101
+ precision=2,
102
+ )
103
+
104
+ gr.Markdown("---")
105
+
106
+ gr.Markdown("### Prédiction")
107
+
108
+ y_out = gr.Number(
109
+ label="Physical experience level notation",
110
+ interactive=False,
111
+ precision=2,
112
+ )
113
+
114
+ # Nouveau champ texte interprétation du niveau
115
+ level_out = gr.Textbox(
116
+ label="Physical level (text)",
117
+ interactive=False,
118
+ lines=1,
119
+ max_lines=1,
120
+ )
121
+
122
+ meta_out = gr.Textbox(
123
+ label="Informations",
124
+ interactive=False,
125
+ lines=4,
126
+ max_lines=8,
127
+ )
128
+
129
+ # ====== Exemples ======
130
+ examples_dicts = ui_examples or [schema.get("example_payload", {})]
131
+
132
+ # --- Tableau complet (pour l'affichage) ---
133
+ df_examples_full = pd.DataFrame(examples_dicts)
134
+
135
+ # On impose Experience_Level en première colonne si elle existe
136
+ if "Experience_Level" in df_examples_full.columns:
137
+ ordered_cols = (
138
+ ["Experience_Level"] +
139
+ [c for c in df_examples_full.columns if c != "Experience_Level"]
140
+ )
141
+ df_examples_full = df_examples_full[ordered_cols]
142
+
143
+ # --- Exemples envoyés au modèle (uniquement les features d’entrée) ---
144
+ df_examples_for_gradio = df_examples_full[names] # on garde seulement les features utiles
145
+ rows = df_examples_for_gradio.values.tolist()
146
+
147
+ gr.Examples(
148
+ examples=rows,
149
+ inputs=comps,
150
+ label="Exemples (sélection rapide)"
151
+ )
152
+
153
+ gr.Markdown("---")
154
+
155
+ # ====== Prédiction ======
156
+
157
+ def _interpret_level(y_val) -> str:
158
+ """Map numeric prediction to textual level."""
159
+ try:
160
+ if y_val is None:
161
+ return ""
162
+ v = float(y_val)
163
+ except Exception:
164
+ return ""
165
+
166
+ if 0 <= v < 1:
167
+ return "Beginner"
168
+ elif 1 <= v < 2:
169
+ return "Intermediate"
170
+ elif 2 <= v < 2.5:
171
+ return "Advanced"
172
+ elif 2.5 <= v <= 3:
173
+ return "Expert"
174
+ return ""
175
+
176
+ def _fn(*vals):
177
+ payload = {k: v for k, v in zip(names, vals)}
178
+
179
+ # Gradio peut renvoyer les valeurs numériques en str → on force
180
+ if "Age" in payload:
181
+ payload["Age"] = float(payload["Age"])
182
+ if "Weight (kg)" in payload:
183
+ payload["Weight (kg)"] = float(payload["Weight (kg)"])
184
+ if "Height (m)" in payload:
185
+ payload["Height (m)"] = float(payload["Height (m)"])
186
+ if "Workout_Frequency (days/week)" in payload:
187
+ payload["Workout_Frequency (days/week)"] = float(
188
+ payload["Workout_Frequency (days/week)"]
189
+ )
190
+
191
+ # 1) prédiction XP via le pipeline existant
192
+ y_xp, meta = predict_single(
193
+ payload=payload,
194
+ internal_expected=internal_expected,
195
+ model=model,
196
+ feature_scaler=feature_scaler,
197
+ target_scaler=target_scaler,
198
+ log_dir=logs_dir,
199
+ model_path=model_path,
200
+ schema=schema,
201
+ target_name=target_name,
202
+ encoder=encoder,
203
+ )
204
+
205
+ # 2) Calcul BMI & Body Fat %
206
+ bmi = None
207
+ fat_pct = None
208
+ try:
209
+ w = float(payload.get("Weight (kg)", 0) or 0)
210
+ h = float(payload.get("Height (m)", 0) or 0)
211
+ if h > 0:
212
+ bmi = round(w / (h ** 2), 2)
213
+
214
+ age_val = payload.get("Age")
215
+ gender_raw = str(payload.get("Gender", "")).lower()
216
+
217
+ if bmi is not None and age_val not in (None, ""):
218
+ age_f = float(age_val)
219
+ # 1 = homme, 0 = femme (IMG formule classique)
220
+ sex_flag = 1.0 if gender_raw.startswith("m") else 0.0
221
+
222
+ fat_pct = 1.20 * bmi + 0.23 * age_f - 10.8 * sex_flag - 5.4
223
+ fat_pct = round(fat_pct, 2)
224
+ except Exception:
225
+ bmi = None
226
+ fat_pct = None
227
+
228
+ # 3) Interprétation textuelle du niveau
229
+ level_text = _interpret_level(y_xp)
230
+
231
+ # 4) Retourner les 5 sorties Gradio
232
+ return y_xp, level_text, bmi, fat_pct, meta
233
+
234
+ btn.click(_fn, comps, [y_out, level_out, bmi_out, fat_out, meta_out])
235
+
236
+ gr.Markdown("---")
237
+
238
+ # ====== Rapport modèle ======
239
+ rep = read_model_report(report_path)
240
+ df_sum = report_summary_df(rep)
241
+ df_mets = report_metrics_df(rep)
242
+
243
+ gr.Markdown("### Machine Learning model evaluation report")
244
+ gr.Dataframe(
245
+ value=df_sum,
246
+ interactive=False,
247
+ wrap=True,
248
+ label="Summary",
249
+ row_count=(0, "dynamic"),
250
+ col_count=df_sum.shape[1],
251
+ )
252
+ gr.Dataframe(
253
+ value=df_mets,
254
+ interactive=False,
255
+ wrap=True,
256
+ label="Metrics by model",
257
+ row_count=(0, "dynamic"),
258
+ col_count=df_mets.shape[1],
259
+ )
260
+ # On renvoie le composant pour que les autres onglets puissent l'utiliser
261
+ return level_out, wf_comp, wt_comp