Spaces:
Sleeping
Sleeping
Commit 1
Browse files- .gitignore +214 -0
- .vscode/settings.json +5 -0
- app.py +3 -0
- main.py +5 -0
- requirements.txt +0 -0
- src/config/features.yaml +58 -0
- src/config/features_processed.yaml +84 -0
- src/config/inference.yaml +159 -0
- src/config/ui_defaults.yaml +12 -0
- src/config/ui_examples.yaml +67 -0
- src/gradio/__init__.py +0 -0
- src/gradio/app.py +104 -0
- src/gradio/config.py +94 -0
- src/gradio/data/corpus_car_FULL_1.txt +1 -0
- src/gradio/data/corpus_car_FULL_2.txt +1 -0
- src/gradio/data/corpus_car_FULL_3.txt +1 -0
- src/gradio/data/dataset_exercices_fusion_20251127_2004.json +0 -0
- src/gradio/data/goal_map.json +9 -0
- src/gradio/data/program_summary.csv +0 -0
- src/gradio/generators/execution_generator.py +345 -0
- src/gradio/generators/gpt2_distillation_text_generator.py +227 -0
- src/gradio/generators/gpt2_fine_tuning_text_generator.py +54 -0
- src/gradio/generators/lstm_text_generator.py +273 -0
- src/gradio/generators/transformer_text_generator.py +552 -0
- src/gradio/helpers/custom_layers.py +200 -0
- src/gradio/helpers/exercices_tab_utilis.py +84 -0
- src/gradio/helpers/log_utils.py +42 -0
- src/gradio/helpers/model_manager.py +245 -0
- src/gradio/helpers/predict_utils.py +130 -0
- src/gradio/helpers/preprocess_utils.py +22 -0
- src/gradio/helpers/report_utils.py +59 -0
- src/gradio/helpers/schema_utils.py +42 -0
- src/gradio/helpers/sqlite_utils.py +80 -0
- src/gradio/model_loader.py +46 -0
- src/gradio/pages/dl_execution_tab.py +190 -0
- src/gradio/pages/dl_tab.py +278 -0
- src/gradio/pages/exercices_tab.py +308 -0
- 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
|