HendSta commited on
Commit
67df1ef
·
1 Parent(s): c9e8958

fix models

Browse files
Files changed (3) hide show
  1. README.md +141 -160
  2. app.py +173 -322
  3. requirements.txt +2 -1
README.md CHANGED
@@ -8,189 +8,170 @@ pinned: false
8
  license: mit
9
  ---
10
 
11
- # MedWin Analyzer 🏥
12
-
13
- Une API intelligente pour l'analyse de rapports médicaux utilisant 3 modèles de Machine Learning spécialisés.
14
-
15
- ## 🚀 Fonctionnalités
16
-
17
- ### 📊 **Modèle 1: HendSta/analyse_medicale**
18
- - **Fonction**: Prédiction de paramètres médicaux
19
- - **Endpoint**: `/predict`
20
- - **Utilisation**: Analyse et classification des paramètres biologiques
21
-
22
- ### ⚠️ **Modèle 2: HendSta/analyse_row**
23
- - **Fonction**: Analyse de risque
24
- - **Endpoint**: `/analyze-risk`
25
- - **Utilisation**: Évaluation du niveau de risque des anomalies biologiques
26
-
27
- ### 🧠 **Modèle 3: HendSta/biomistral-finetuned-fullv3**
28
- - **Fonction**: Prédiction de maladies
29
- - **Endpoint**: `/predict-disease`
30
- - **Utilisation**: Diagnostic basé sur les paramètres anormaux
31
-
32
- ## 📋 Endpoints API
33
-
34
- ### 1. **GET /** - Informations générales
35
- ```bash
36
- curl https://huggingface.co/spaces/HendSta/MedWin-Analyzer
37
- ```
38
-
39
- ### 2. **POST /predict** - Prédiction de paramètres
40
- ```bash
41
- curl -X POST "https://huggingface.co/spaces/HendSta/MedWin-Analyzer/predict" \
42
- -H "Content-Type: application/json" \
43
- -d '{
44
- "CodeParametre": "gly",
45
- "ValeurActuelle": 6.2,
46
- "Unite": "mmol/L",
47
- "ValeursUsuelles": "3.9-6.1",
48
- "ValeurUsuelleMin": 3.9,
49
- "ValeurUsuelleMax": 6.1
50
- }'
51
- ```
52
-
53
- ### 3. **POST /upload-pdf** - Analyse de fichiers PDF/XML
54
- ```bash
55
- curl -X POST "https://huggingface.co/spaces/HendSta/MedWin-Analyzer/upload-pdf" \
56
- -F "file=@rapport_medical.pdf"
57
- ```
58
-
59
- ### 4. **POST /analyze-risk** - Analyse de risque
60
- ```bash
61
- curl -X POST "https://huggingface.co/spaces/HendSta/MedWin-Analyzer/analyze-risk" \
62
- -H "Content-Type: application/json" \
63
- -d '{
64
- "CodeParametre": "gly",
65
- "ValeurActuelle": 6.2,
66
- "Unite": "mmol/L",
67
- "ValeursUsuelles": "3.9-6.1",
68
- "ValeurUsuelleMin": 3.9,
69
- "ValeurUsuelleMax": 6.1,
70
- "CodParametre": "GLY"
71
- }'
72
- ```
73
-
74
- ### 5. **POST /predict-disease** - Prédiction de maladies
75
- ```bash
76
- curl -X POST "https://huggingface.co/spaces/HendSta/MedWin-Analyzer/predict-disease" \
77
- -H "Content-Type: application/json" \
78
- -d '{
79
- "risk_results": [
80
- {
81
- "statut_risque": "ÉLEVÉ",
82
- "degre_risque": "Modéré"
83
- }
84
- ],
85
- "analysis_result": [
86
- {
87
- "LibParametre": "Glycémie",
88
- "ValeurActuelle": 6.2,
89
- "Unite": "mmol/L",
90
- "ValeursUsuelles": "3.9-6.1"
91
- }
92
- ]
93
- }'
94
- ```
95
-
96
- ## 🔧 Technologies utilisées
97
-
98
- - **FastAPI**: Framework web moderne et rapide
99
- - **Transformers**: Modèles de langage Hugging Face
100
- - **PyTorch**: Deep Learning
101
- - **Pandas**: Manipulation de données
102
- - **Scikit-learn**: Machine Learning
103
- - **PDFPlumber**: Extraction de texte PDF
104
- - **Joblib**: Sauvegarde/chargement de modèles
105
-
106
- ## 📁 Structure du projet
107
 
108
- ```
109
- MedWin-Analyzer/
110
- ├── app.py # Application FastAPI principale
111
- ├── requirements.txt # Dépendances Python
112
- ├── Dockerfile # Configuration Docker
113
- └── README.md # Documentation
114
- ```
115
 
116
- ## 🚀 Démarrage rapide
117
 
118
- 1. **Cloner le repository**:
119
- ```bash
120
- git clone https://huggingface.co/spaces/HendSta/MedWin-Analyzer
121
- cd MedWin-Analyzer
122
- ```
123
 
124
- 2. **Installer les dépendances**:
125
- ```bash
126
- pip install -r requirements.txt
127
- ```
 
128
 
129
- 3. **Lancer l'application**:
130
- ```bash
131
- python app.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  ```
133
 
134
- L'API sera disponible sur `http://localhost:7860`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
- ## 📊 Exemples d'utilisation
 
 
 
 
137
 
138
- ### Analyse d'un rapport PDF
139
- ```python
140
- import requests
141
 
142
- # Upload d'un fichier PDF
143
- with open('rapport.pdf', 'rb') as f:
144
- files = {'file': f}
145
- response = requests.post(
146
- 'https://huggingface.co/spaces/HendSta/MedWin-Analyzer/upload-pdf',
147
- files=files
148
- )
149
- results = response.json()
150
- print(results)
 
 
 
 
 
 
 
 
 
151
  ```
152
 
153
- ### Analyse de risque
154
- ```python
155
- import requests
 
 
 
 
 
 
 
 
 
 
 
156
 
157
- data = {
158
- "CodeParametre": "crea",
159
- "ValeurActuelle": 120,
160
- "Unite": "µmol/L",
161
- "ValeursUsuelles": "60-110",
162
- "ValeurUsuelleMin": 60,
163
- "ValeurUsuelleMax": 110,
164
- "CodParametre": "CREA"
 
 
 
 
 
 
165
  }
 
166
 
167
- response = requests.post(
168
- 'https://huggingface.co/spaces/HendSta/MedWin-Analyzer/analyze-risk',
169
- json=data
170
- )
171
- risk_analysis = response.json()
172
- print(risk_analysis)
 
173
  ```
174
 
175
- ## ⚠️ Avertissements
 
 
 
 
 
 
176
 
177
- - Cette API est destinée à des fins éducatives et de recherche
178
- - Les résultats ne constituent pas un diagnostic médical
179
- - Consultez toujours un professionnel de santé pour toute décision médicale
180
- - Les modèles sont basés sur des données d'entraînement et peuvent avoir des limitations
181
 
182
- ## 📄 Licence
183
 
184
- MIT License - Voir le fichier LICENSE pour plus de détails.
 
 
 
 
 
 
 
185
 
186
- ## 🤝 Contribution
187
 
188
- Les contributions sont les bienvenues ! N'hésitez pas à ouvrir une issue ou une pull request.
 
 
 
189
 
190
- ## 📞 Support
191
 
192
- Pour toute question ou problème, veuillez ouvrir une issue sur le repository Hugging Face.
193
 
194
- ---
 
 
 
 
 
 
 
195
 
196
- **Développé avec ❤️ pour la communauté médicale**
 
 
 
8
  license: mit
9
  ---
10
 
11
+ # MedWin-Analyzer - Hugging Face Space
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
+ Ce repository contient une application FastAPI déployée sur Hugging Face Spaces pour l'analyse de rapports médicaux.
 
 
 
 
 
 
14
 
15
+ ## 🚀 Modèles Utilisés
16
 
17
+ L'application utilise trois modèles hébergés sur Hugging Face :
 
 
 
 
18
 
19
+ 1. **HendSta/analyse_medicale** - Modèle d'analyse médicale pour la classification des paramètres
20
+ 2. **HendSta/analyse_row** - Modèle d'analyse de risque pour évaluer les niveaux de risque
21
+ 3. **HendSta/biomistral-finetuned-fullv3** - Modèle LLM pour l'analyse textuelle avancée
22
+
23
+ ## 📋 Endpoints Disponibles
24
 
25
+ ### 1. Health Check
26
+ ```
27
+ GET /health
28
+ ```
29
+ Vérifie que tous les modèles sont chargés correctement.
30
+
31
+ **Réponse :**
32
+ ```json
33
+ {
34
+ "status": "healthy",
35
+ "models_loaded": {
36
+ "analyse_medicale_model": true,
37
+ "analyze_risk_model": true,
38
+ "llm_model": true,
39
+ "llm_tokenizer": true
40
+ },
41
+ "message": "Tous les modèles sont chargés"
42
+ }
43
  ```
44
 
45
+ ### 2. Prédiction Simple
46
+ ```
47
+ POST /predict
48
+ ```
49
+ Effectue une prédiction sur un seul paramètre.
50
+
51
+ **Body :**
52
+ ```json
53
+ {
54
+ "CodeParametre": "glucose",
55
+ "ValeurActuelle": 120.0,
56
+ "Unite": "mg/dL",
57
+ "ValeursUsuelles": "70-100",
58
+ "ValeurUsuelleMin": 70.0,
59
+ "ValeurUsuelleMax": 100.0,
60
+ "ValeurAnterieure": 110.0,
61
+ "DateAnterieure": "01/01/2024"
62
+ }
63
+ ```
64
 
65
+ ### 3. Upload PDF/XML
66
+ ```
67
+ POST /upload-pdf
68
+ ```
69
+ Traite un fichier PDF ou XML et retourne l'analyse de tous les paramètres.
70
 
71
+ **Body :** `multipart/form-data` avec le fichier
 
 
72
 
73
+ ### 4. Analyse de Risque
74
+ ```
75
+ POST /analyze-risk
76
+ ```
77
+ Analyse le niveau de risque d'un paramètre médical.
78
+
79
+ **Body :**
80
+ ```json
81
+ {
82
+ "CodeParametre": "glucose",
83
+ "ValeurActuelle": 120.0,
84
+ "Unite": "mg/dL",
85
+ "ValeursUsuelles": "70-100",
86
+ "ValeurUsuelleMin": 70.0,
87
+ "ValeurUsuelleMax": 100.0,
88
+ "ValeurAnterieure": 110.0,
89
+ "CodParametre": "GLU"
90
+ }
91
  ```
92
 
93
+ **Réponse :**
94
+ ```json
95
+ {
96
+ "parametre": "glucose",
97
+ "valeur_actuelle": 120.0,
98
+ "unite": "mg/dL",
99
+ "valeur_anterieure": 110.0,
100
+ "valeurs_usuelles": "70-100",
101
+ "statut_risque": "ÉLEVÉ",
102
+ "degre_risque": "Modéré",
103
+ "tendance": "En hausse",
104
+ "conseil": "Surveillance recommandée. Le glucose est élevé avec un risque modéré."
105
+ }
106
+ ```
107
 
108
+ ### 5. Analyse LLM
109
+ ```
110
+ POST /llm-analysis
111
+ ```
112
+ Utilise le modèle LLM pour fournir une analyse textuelle détaillée.
113
+
114
+ **Body :**
115
+ ```json
116
+ {
117
+ "CodeParametre": "glucose",
118
+ "ValeurActuelle": 120.0,
119
+ "Unite": "mg/dL",
120
+ "ValeursUsuelles": "70-100",
121
+ "ValeurAnterieure": 110.0
122
  }
123
+ ```
124
 
125
+ **Réponse :**
126
+ ```json
127
+ {
128
+ "parametre": "glucose",
129
+ "analyse_llm": "Analyse détaillée générée par le LLM...",
130
+ "prompt_utilise": "Prompt utilisé pour la génération"
131
+ }
132
  ```
133
 
134
+ ## 🔧 Configuration
135
+
136
+ ### Variables d'Environnement
137
+ - `HF_TOKEN` : Token Hugging Face (optionnel pour les modèles publics)
138
+
139
+ ### Dépendances
140
+ Voir `requirements.txt` pour la liste complète des dépendances.
141
 
142
+ ## 🚀 Déploiement
 
 
 
143
 
144
+ Cette application est configurée pour être déployée automatiquement sur Hugging Face Spaces.
145
 
146
+ ### Structure des Fichiers
147
+ ```
148
+ MedWin-Analyzer/
149
+ ├── app.py # Application FastAPI principale
150
+ ├── requirements.txt # Dépendances Python
151
+ ├── Dockerfile # Configuration Docker
152
+ └── README.md # Documentation
153
+ ```
154
 
155
+ ## 📊 Utilisation
156
 
157
+ 1. **Démarrage automatique** : Les modèles sont chargés automatiquement au démarrage
158
+ 2. **Health check** : Utilisez `/health` pour vérifier l'état des modèles
159
+ 3. **Upload de fichiers** : Supporte les formats PDF et XML
160
+ 4. **Analyse en temps réel** : Tous les endpoints fournissent des réponses immédiates
161
 
162
+ ## 🔍 Dépannage
163
 
164
+ ### Erreurs Courantes
165
 
166
+ 1. **Modèles non chargés** : Vérifiez la connexion internet et les permissions
167
+ 2. **Erreur de format** : Assurez-vous que les fichiers PDF/XML sont valides
168
+ 3. **Timeout** : Les modèles LLM peuvent prendre du temps pour la première génération
169
+
170
+ ### Logs
171
+ Les logs de chargement des modèles sont affichés au démarrage de l'application.
172
+
173
+ ## 📝 Notes
174
 
175
+ - Les modèles sont téléchargés automatiquement depuis Hugging Face au premier démarrage
176
+ - Le cache des modèles est conservé pour les démarrages suivants
177
+ - L'application gère automatiquement les erreurs de chargement des modèles
app.py CHANGED
@@ -18,23 +18,15 @@ import xml.etree.ElementTree as ET
18
  from fastapi.responses import JSONResponse
19
  from sklearn.base import BaseEstimator, TransformerMixin
20
  import sys
21
- from dotenv import load_dotenv
22
  from huggingface_hub import hf_hub_download
23
- import torch
24
  from transformers import AutoTokenizer, AutoModelForCausalLM
 
25
 
26
- # Charger les variables d'environnement
27
- load_dotenv()
28
-
29
- app = FastAPI(
30
- title="MedWin Analyzer",
31
- description="API pour l'analyse de rapports médicaux avec 3 modèles ML",
32
- version="1.0.0"
33
- )
34
 
35
  app.add_middleware(
36
  CORSMiddleware,
37
- allow_origins=["*"], # Pour Hugging Face Spaces
38
  allow_credentials=True,
39
  allow_methods=["*"],
40
  allow_headers=["*"],
@@ -62,63 +54,54 @@ class NumericConverter(BaseEstimator, TransformerMixin):
62
 
63
  sys.modules['__main__'].NumericConverter = NumericConverter
64
 
65
- # Variables globales pour les modèles
66
- pipeline = None
67
- risk_model = None
68
- llm_model = None
69
- llm_tokenizer = None
70
- models_loaded = False
71
-
72
- def load_models():
73
  """Charge tous les modèles depuis Hugging Face"""
74
- global pipeline, risk_model, llm_model, llm_tokenizer, models_loaded
75
 
76
- if models_loaded:
77
- return pipeline, risk_model, llm_model, llm_tokenizer
78
 
 
79
  try:
80
- print("🔄 Chargement des modèles depuis Hugging Face...")
81
-
82
- # 1. Modèle d'analyse médicale (HendSta/analyse_medicale)
83
- print("📊 Chargement du modèle d'analyse médicale...")
84
- pipeline_path = hf_hub_download(
85
  repo_id="HendSta/analyse_medicale",
86
  filename="modele_analyse_medicale_final.joblib"
87
  )
88
- pipeline = joblib.load(pipeline_path)
89
- print("✅ Modèle d'analyse médicale chargé!")
90
-
91
- # 2. Modèle d'analyse de risque (HendSta/analyse_row)
92
- print("⚠️ Chargement du modèle d'analyse de risque...")
93
- risk_model_path = hf_hub_download(
 
 
 
94
  repo_id="HendSta/analyse_row",
95
  filename="analyze_row_final.joblib"
96
  )
97
- risk_model = joblib.load(risk_model_path)
98
- print("✅ Modèle d'analyse de risque chargé!")
99
-
100
- # 3. Modèle LLM BioMistral (HendSta/biomistral-finetuned-fullv3)
101
- print("🧠 Chargement du modèle LLM BioMistral...")
 
 
 
102
  llm_tokenizer = AutoTokenizer.from_pretrained("HendSta/biomistral-finetuned-fullv3")
103
- llm_model = AutoModelForCausalLM.from_pretrained(
104
- "HendSta/biomistral-finetuned-fullv3",
105
- device_map="auto" if torch.cuda.is_available() else "cpu",
106
- torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
107
- low_cpu_mem_usage=True
108
- )
109
-
110
- if llm_tokenizer.pad_token is None:
111
- llm_tokenizer.pad_token = llm_tokenizer.eos_token
112
-
113
- print("✅ Modèle LLM BioMistral chargé!")
114
-
115
- models_loaded = True
116
- print("🎉 Tous les modèles chargés avec succès!")
117
- return pipeline, risk_model, llm_model, llm_tokenizer
118
-
119
  except Exception as e:
120
- print(f"❌ Erreur lors du chargement des modèles: {str(e)}")
121
- return None, None, None, None
 
 
 
 
 
 
 
 
 
122
 
123
  # Créer un imputer pour gérer les valeurs NaN
124
  imputer = SimpleImputer(strategy='constant', fill_value=0)
@@ -178,7 +161,7 @@ TYPE_ANALYSES = {
178
  "dosage des vitamines": ["dosage des vitamines"]
179
  }
180
 
181
- # Regex patterns
182
  REGEX_DATE = r"\b(\d{2}/\d{2}/\d{4})\b"
183
  REGEX_PATIENT = r"(?i)nom\s*:\s*(.*)"
184
  REGEX_MEDECIN = r"(?i)demandé par\s*:\s*(.*)"
@@ -194,7 +177,7 @@ UNIT_MAPPING = {
194
  'g/dl': 'g/dL',
195
  'mmol/l': 'mmol/L',
196
  'pmol/l': 'pmol/L'
197
- }
198
 
199
  # ==== Helper Functions ====
200
  def normaliser_type_analyse(texte):
@@ -230,6 +213,7 @@ def extract_min_max(valeur_usuelles):
230
  valeur_usuelles = valeur_usuelles.strip()
231
 
232
  # Nettoyer les espaces à l'intérieur des nombres dans la chaîne
 
233
  valeur_usuelles = re.sub(r'(?<=\d)\s+(?=\d)', '', valeur_usuelles)
234
 
235
  range_pattern = r'(\d+(?:[.,]\d+)?)\s*-\s*(\d+(?:[.,]\d+)?)'
@@ -309,7 +293,7 @@ def extract_patient_info(text: str) -> Dict[str, str]:
309
  return patient_info
310
 
311
  def extract_all_fields_from_text(text: str) -> list:
312
- """Extrait tous les paramètres et valeurs du texte nettoyé."""
313
  results = []
314
  lines = text.splitlines()
315
  for line in lines:
@@ -318,6 +302,7 @@ def extract_all_fields_from_text(text: str) -> list:
318
  continue
319
 
320
  # Nettoyer les motifs "X % Soit :"
 
321
  soit_match = re.search(r'^([\w\s\.]+)\s+(\d+)\s*%\s*Soit\s*:\s*(.+)$', line, re.IGNORECASE)
322
  if soit_match:
323
  param_name = soit_match.group(1).strip()
@@ -433,164 +418,34 @@ def to_native(val):
433
  return val.item()
434
  return val
435
 
436
- def analyze_abnormal_parameters(abnormal_params):
437
- """
438
- Analyse les paramètres anormaux et retourne des prédictions de maladies basées sur des règles
439
- """
440
- diseases = []
441
-
442
- # Dictionnaire des maladies associées aux paramètres
443
- disease_patterns = {
444
- 'diabète': ['GLY', 'GLUCOSE', 'HBA1C', 'HBA2C', 'glycémie'],
445
- 'hypercholestérolémie': ['CHOLESTEROL', 'CT', 'LDL', 'HDL', 'TG', 'TRIGLYCERIDES'],
446
- 'insuffisance rénale': ['CREA', 'CREATININE', 'UREE', 'URI'],
447
- 'anémie': ['HEM1', 'NFS5', 'NFS6', 'HEMOGLOBINE'],
448
- 'hyperthyroïdie': ['TSH', 'T3', 'T4'],
449
- 'hypothyroïdie': ['TSH'],
450
- 'inflammation': ['CRP', 'VS', 'FIBRINOGENE'],
451
- 'problèmes hépatiques': ['AST', 'ALT', 'ALAT', 'ASAT', 'BILIRUBINE'],
452
- 'problèmes cardiaques': ['TROPONINE', 'CPK', 'BNP']
453
- }
454
-
455
- # Analyser chaque paramètre anormal
456
- for param in abnormal_params:
457
- param_name = param['name'].upper()
458
- status = param['status']
459
- value = param['value']
460
-
461
- # Chercher des correspondances avec les patterns de maladies
462
- for disease, patterns in disease_patterns.items():
463
- for pattern in patterns:
464
- if pattern.upper() in param_name:
465
- if disease not in diseases:
466
- diseases.append(disease)
467
- break
468
-
469
- # Ajouter des analyses spécifiques
470
- if diseases:
471
- return [f"Possibilité de {disease.replace('_', ' ')}" for disease in diseases]
472
- else:
473
- return ["Anomalies biologiques détectées nécessitant une évaluation médicale"]
474
-
475
- def extract_valeurs_usuelles_xml(val):
476
- """Extrait les bornes min/max des valeurs usuelles depuis un format XML."""
477
- if not isinstance(val, str) or val.strip() == "":
478
- return None, None
479
- val = val.lower().replace(',', '.').strip()
480
-
481
- try:
482
- if '-' in val:
483
- parts = val.split('-')
484
- return float(parts[0].strip()), float(parts[1].strip())
485
- elif 'inf à' in val:
486
- return None, float(re.sub(r"[^\d.]", "", val))
487
- elif 'sup à' in val or '>' in val:
488
- return float(re.sub(r"[^\d.]", "", val)), None
489
- except:
490
- return None, None
491
-
492
- return None, None
493
-
494
- def parse_xml_file(xml_bytes: bytes) -> list:
495
- """Parse un fichier XML et retourne les résultats au format attendu par l'API."""
496
  try:
497
- # Utiliser BytesIO pour lire les bytes comme un fichier
498
- tree = ET.parse(io.BytesIO(xml_bytes))
499
- root = tree.getroot()
 
 
 
 
500
 
501
- results = []
502
-
503
- demande = root.find(".//Demande")
504
- if demande is None:
505
- raise HTTPException(status_code=400, detail="Format XML non reconnu: élément 'Demande' introuvable")
506
-
507
- nom_patient = demande.findtext("NomPatient", "").strip()
508
- prenom_patient = demande.findtext("PrenomPatient", "").strip()
509
- patient_name = f"{nom_patient} {prenom_patient}".strip()
510
- patient_name = nettoyer_nom_patient(patient_name)
511
- medecin = demande.findtext("MedecinPrescripteur", "").strip()
512
- date_analyse = demande.findtext("DateSaisie", "").strip()
513
-
514
- # Convertir la date si nécessaire
515
- if re.match(r'^\d{4}-\d{2}-\d{2}$', date_analyse):
516
- parts = date_analyse.split('-')
517
- date_analyse = f"{parts[2]}/{parts[1]}/{parts[0]}"
518
-
519
- for examen in demande.findall(".//Examen"):
520
- famille = examen.findtext("Famille", "").strip()
521
- code_analyse = examen.findtext("CodeAnalyse", "").strip()
522
- lib_analyse = examen.findtext("LibAnalyse", "").strip()
523
-
524
- for res in examen.findall("Resultat"):
525
- cod_param = res.findtext("CodParametre", "").strip()
526
- lib_param = res.findtext("LibParametre", "").strip()
527
- valeur = res.findtext("Valeur", "").replace(",", ".").strip()
528
- unite = res.findtext("Unite", "").strip()
529
- val_usuelle = res.findtext("ValeurUsuelles", "").strip()
530
-
531
- val_min, val_max = extract_valeurs_usuelles_xml(val_usuelle)
532
-
533
- # Normalisation des valeurs
534
- try:
535
- valeur_actuelle = normalize_numeric_values(valeur)
536
- except ValueError:
537
- valeur_actuelle = ''
538
-
539
- results.append({
540
- "CodeParametre": cod_param.lower(),
541
- "ValeurActuelle": valeur_actuelle,
542
- "Unite": unite,
543
- "ValeursUsuelles": val_usuelle,
544
- "ValeurUsuelleMin": val_min,
545
- "ValeurUsuelleMax": val_max,
546
- "ValeurAnterieure": None,
547
- "DateAnterieure": '',
548
- "NomPatient": patient_name,
549
- "Medecin": medecin,
550
- "DateAnalyse": date_analyse,
551
- "CodParametre": cod_param, # Champ prédit (copie du code paramètre)
552
- "LIBMEDWINabrege": cod_param, # Pourrait être différent, dépend du modèle
553
- "LibParametre": lib_param,
554
- "FAMILLE": famille
555
- })
556
-
557
- if not results:
558
- raise HTTPException(status_code=400, detail="Aucun paramètre reconnu dans le XML")
559
-
560
- return results
561
  except Exception as e:
562
- raise HTTPException(status_code=400, detail=f"Erreur lors du traitement du XML: {str(e)}")
563
-
564
- # ==== API Endpoints ====
565
- @app.on_event("startup")
566
- async def startup_event():
567
- """Événement de démarrage"""
568
- print("🚀 Démarrage du serveur MedWin Analyzer...")
569
- print("📥 Chargement des modèles depuis Hugging Face...")
570
- load_models()
571
- print("✅ Serveur prêt!")
572
-
573
- @app.get("/")
574
- def greet_json():
575
- """Endpoint de base pour tester l'API"""
576
- return {
577
- "message": "MedWin Analyzer API",
578
- "version": "1.0.0",
579
- "description": "API pour l'analyse de rapports médicaux avec 3 modèles ML",
580
- "endpoints": {
581
- "/predict": "Prédiction de paramètres médicaux",
582
- "/upload-pdf": "Analyse de fichiers PDF",
583
- "/analyze-risk": "Analyse de risque",
584
- "/predict-disease": "Prédiction de maladies"
585
  }
586
- }
587
 
588
  @app.post("/predict", response_model=PredictionResult)
589
  def predict(data: InputData):
590
- """Prédit les paramètres médicaux avec le modèle HendSta/analyse_medicale"""
591
- if pipeline is None:
592
- raise HTTPException(status_code=500, detail="Modèle non chargé")
593
-
594
  df = pd.DataFrame([data.dict()])
595
  preds = pipeline.predict(df)[0]
596
  return PredictionResult(
@@ -603,10 +458,6 @@ def predict(data: InputData):
603
 
604
  @app.post("/upload-pdf", response_model=List[PredictionResult])
605
  async def upload_file(file: UploadFile = File(...)):
606
- """Analyse un fichier PDF ou XML et retourne les prédictions"""
607
- if pipeline is None:
608
- raise HTTPException(status_code=500, detail="Modèle non chargé")
609
-
610
  content = await file.read()
611
  file_extension = file.filename.split('.')[-1].lower()
612
 
@@ -615,7 +466,7 @@ async def upload_file(file: UploadFile = File(...)):
615
  if file.content_type != "application/pdf":
616
  raise HTTPException(status_code=400, detail="Le fichier doit être au format PDF")
617
 
618
- # Traitement PDF
619
  extracted_text = extract_text_from_pdf_bytes(content)
620
  cleaned_text = nettoyer_text(extracted_text)
621
  patient_info = extract_patient_info(cleaned_text)
@@ -678,14 +529,15 @@ async def upload_file(file: UploadFile = File(...)):
678
 
679
  @app.post("/analyze-risk")
680
  def analyze_risk(param: dict = Body(...)):
681
- """Analyse le risque avec le modèle HendSta/analyse_row"""
682
- if risk_model is None:
683
- raise HTTPException(status_code=500, detail="Modèle de risque non chargé")
684
-
 
685
  # Préparer le DataFrame à partir du paramètre reçu
686
  df_test = pd.DataFrame([param])
687
 
688
- # Préparation des features dérivées
689
  df_result = df_test.copy()
690
  try:
691
  df_result['ValeurAnterieure'] = pd.to_numeric(df_result['ValeurAnterieure'], errors='coerce')
@@ -727,7 +579,7 @@ def analyze_risk(param: dict = Body(...)):
727
  features_for_ml = df_result[['DeltaValeurPrecedente', 'RatioValeurPrecedente',
728
  'PourcentageValeurMin', 'PourcentageValeurMax',
729
  'EcartNormalise', 'ValeurActuelle', 'CodeParametre']]
730
- predicted_risk_num = risk_model.predict(features_for_ml)[0]
731
  risk_map = {0: 'Aucun', 1: 'Faible', 2: 'Modéré', 3: 'Élevé'}
732
  degre_risque = risk_map.get(int(predicted_risk_num), 'Inconnu')
733
 
@@ -766,108 +618,107 @@ def analyze_risk(param: dict = Body(...)):
766
  "conseil": to_native(conseil)
767
  }
768
 
769
- @app.post("/predict-disease")
770
- def predict_disease(data: dict = Body(...)):
771
- """Prédit les maladies avec le modèle HendSta/biomistral-finetuned-fullv3"""
772
- try:
773
- print("🔍 Début de l'analyse de prédiction de maladie...")
774
- print(f"Données reçues: {len(data.get('risk_results', []))} résultats de risque")
775
-
776
- # Vérifier si tous les statuts sont NORMAL
777
- risk_results = data.get('risk_results', [])
778
- abnormal_count = 0
779
-
780
- for i, risk_result in enumerate(risk_results):
781
- if risk_result and risk_result.get('statut_risque') != 'NORMAL':
782
- abnormal_count += 1
783
- print(f"Paramètre anormal détecté: {risk_result.get('statut_risque')}")
784
-
785
- print(f"Nombre de paramètres anormaux: {abnormal_count}")
786
-
787
- if abnormal_count == 0:
788
- return {
789
- "disease_prediction": "Aucune maladie détectée",
790
- "confidence": "Élevée",
791
- "explanation": "Tous les paramètres biologiques sont dans les plages normales.",
792
- "recommendations": "Continuez à maintenir un mode de vie sain."
793
- }
794
-
795
- # Pour les cas anormaux, utiliser le modèle LLM BioMistral
796
- print("🔍 Analyse des paramètres anormaux avec le modèle LLM...")
797
-
798
- # Préparer le texte des paramètres anormaux
799
- abnormal_params = []
800
- analysis_result = data.get('analysis_result', [])
801
-
802
- for i, risk_result in enumerate(risk_results):
803
- if risk_result and risk_result.get('statut_risque') != 'NORMAL':
804
- if i < len(analysis_result):
805
- param_data = analysis_result[i]
806
- param_name = param_data.get('LibParametre', param_data.get('CodParametre', 'Paramètre'))
807
- current_value = param_data.get('ValeurActuelle', '')
808
- unit = param_data.get('Unite', '')
809
- status = risk_result.get('statut_risque', '')
810
- normal_range = param_data.get('ValeursUsuelles', '')
811
-
812
- abnormal_params.append(
813
- f"- {param_name} : {current_value} {unit} ({status}) | Valeur usuelle : ({normal_range})"
814
- )
815
-
816
- print(f"Paramètres anormaux identifiés: {len(abnormal_params)}")
817
-
818
- if not abnormal_params:
819
- return {
820
- "disease_prediction": "Aucune maladie détectée",
821
- "confidence": "Élevée",
822
- "explanation": "Aucun paramètre anormal significatif détecté.",
823
- "recommendations": "Continuez à maintenir un mode de vie sain."
824
- }
825
-
826
- # Utiliser l'analyse basée sur des règles (mode fallback)
827
- print("🧠 Analyse basée sur des règles médicales...")
828
-
829
- diseases = analyze_abnormal_parameters([{
830
- 'name': param.split(' : ')[0].replace('- ', ''),
831
- 'value': param.split(' : ')[1].split(' ')[0] if ' : ' in param else '',
832
- 'unit': param.split(' ')[2] if len(param.split(' : ')) > 1 and len(param.split(' : ')[1].split(' ')) > 2 else '',
833
- 'status': param.split('(')[1].split(')')[0] if '(' in param and ')' in param else '',
834
- 'normal_range': param.split('(')[-1].split(')')[0] if '(' in param and ')' in param else ''
835
- } for param in abnormal_params])
836
-
837
- if diseases:
838
- prediction_text = "\n".join(diseases)
839
- confidence = "Modérée"
840
- explanation = "Analyse basée sur les paramètres anormaux détectés."
841
- recommendations = "Consultez un professionnel de santé pour confirmation et suivi."
842
- else:
843
- prediction_text = "Anomalies biologiques détectées nécessitant une évaluation médicale approfondie."
844
- confidence = "Faible"
845
- explanation = "Les paramètres anormaux nécessitent une interprétation médicale spécialisée."
846
- recommendations = "Consultez immédiatement un professionnel de santé."
847
-
848
  return {
849
- "disease_prediction": prediction_text,
850
- "confidence": confidence,
851
- "explanation": explanation,
852
- "recommendations": recommendations
 
 
853
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
854
 
855
- except Exception as e:
856
- print(f"Erreur lors de la prédiction de maladie: {str(e)}")
857
- import traceback
858
- traceback.print_exc()
859
- return {
860
- "disease_prediction": "Erreur lors de l'analyse",
861
- "confidence": "Faible",
862
- "explanation": f"Erreur technique: {str(e)}",
863
- "recommendations": "Veuillez réessayer ou consulter un professionnel de santé."
864
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
865
 
866
- if __name__ == "__main__":
867
- import uvicorn
868
- uvicorn.run(
869
- "app:app",
870
- host="0.0.0.0",
871
- port=7860,
872
- reload=False
873
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  from fastapi.responses import JSONResponse
19
  from sklearn.base import BaseEstimator, TransformerMixin
20
  import sys
 
21
  from huggingface_hub import hf_hub_download
 
22
  from transformers import AutoTokenizer, AutoModelForCausalLM
23
+ import torch
24
 
25
+ app = FastAPI()
 
 
 
 
 
 
 
26
 
27
  app.add_middleware(
28
  CORSMiddleware,
29
+ allow_origins=["http://localhost:4200"], # URL de votre frontend Angular
30
  allow_credentials=True,
31
  allow_methods=["*"],
32
  allow_headers=["*"],
 
54
 
55
  sys.modules['__main__'].NumericConverter = NumericConverter
56
 
57
+ # Charger les modèles ML depuis Hugging Face
58
+ def load_models_from_hf():
 
 
 
 
 
 
59
  """Charge tous les modèles depuis Hugging Face"""
60
+ global pipeline, analyze_risk_model, llm_tokenizer, llm_model
61
 
62
+ print("Loading models from Hugging Face...")
 
63
 
64
+ # Charger le modèle d'analyse médicale
65
  try:
66
+ model_path = hf_hub_download(
 
 
 
 
67
  repo_id="HendSta/analyse_medicale",
68
  filename="modele_analyse_medicale_final.joblib"
69
  )
70
+ pipeline = joblib.load(model_path)
71
+ print("✅ Modèle d'analyse médicale chargé avec succès")
72
+ except Exception as e:
73
+ print(f"❌ Erreur lors du chargement du modèle d'analyse médicale: {e}")
74
+ raise
75
+
76
+ # Charger le modèle d'analyse de risque
77
+ try:
78
+ analyze_risk_model_path = hf_hub_download(
79
  repo_id="HendSta/analyse_row",
80
  filename="analyze_row_final.joblib"
81
  )
82
+ analyze_risk_model = joblib.load(analyze_risk_model_path)
83
+ print("✅ Modèle d'analyse de risque chargé avec succès")
84
+ except Exception as e:
85
+ print(f"❌ Erreur lors du chargement du modèle d'analyse de risque: {e}")
86
+ raise
87
+
88
+ # Charger le modèle LLM
89
+ try:
90
  llm_tokenizer = AutoTokenizer.from_pretrained("HendSta/biomistral-finetuned-fullv3")
91
+ llm_model = AutoModelForCausalLM.from_pretrained("HendSta/biomistral-finetuned-fullv3")
92
+ print("✅ Modèle LLM chargé avec succès")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  except Exception as e:
94
+ print(f"❌ Erreur lors du chargement du modèle LLM: {e}")
95
+ raise
96
+
97
+ # Initialiser les modèles avec gestion d'erreur
98
+ try:
99
+ load_models_from_hf()
100
+ print("🎉 Tous les modèles ont été chargés avec succès!")
101
+ except Exception as e:
102
+ print(f"💥 Erreur critique lors du chargement des modèles: {e}")
103
+ print("L'application ne peut pas démarrer sans les modèles.")
104
+ raise
105
 
106
  # Créer un imputer pour gérer les valeurs NaN
107
  imputer = SimpleImputer(strategy='constant', fill_value=0)
 
161
  "dosage des vitamines": ["dosage des vitamines"]
162
  }
163
 
164
+ # Regex patterns - Amélioré pour capturer les nombres avec beaucoup de séparateurs
165
  REGEX_DATE = r"\b(\d{2}/\d{2}/\d{4})\b"
166
  REGEX_PATIENT = r"(?i)nom\s*:\s*(.*)"
167
  REGEX_MEDECIN = r"(?i)demandé par\s*:\s*(.*)"
 
177
  'g/dl': 'g/dL',
178
  'mmol/l': 'mmol/L',
179
  'pmol/l': 'pmol/L'
180
+ }
181
 
182
  # ==== Helper Functions ====
183
  def normaliser_type_analyse(texte):
 
213
  valeur_usuelles = valeur_usuelles.strip()
214
 
215
  # Nettoyer les espaces à l'intérieur des nombres dans la chaîne
216
+ # avant de faire l'extraction
217
  valeur_usuelles = re.sub(r'(?<=\d)\s+(?=\d)', '', valeur_usuelles)
218
 
219
  range_pattern = r'(\d+(?:[.,]\d+)?)\s*-\s*(\d+(?:[.,]\d+)?)'
 
293
  return patient_info
294
 
295
  def extract_all_fields_from_text(text: str) -> list:
296
+ """Extrait tous les paramètres et valeurs du texte nettoyé, y compris valeur antérieure et date antérieure si présentes sur la même ligne."""
297
  results = []
298
  lines = text.splitlines()
299
  for line in lines:
 
302
  continue
303
 
304
  # Nettoyer les motifs "X % Soit :"
305
+ # On conserve uniquement le nom du paramètre au début et ce qui suit "Soit :" s'il est présent
306
  soit_match = re.search(r'^([\w\s\.]+)\s+(\d+)\s*%\s*Soit\s*:\s*(.+)$', line, re.IGNORECASE)
307
  if soit_match:
308
  param_name = soit_match.group(1).strip()
 
418
  return val.item()
419
  return val
420
 
421
+ # ==== API Endpoints ====
422
+ @app.get("/health")
423
+ def health_check():
424
+ """Vérifie que tous les modèles sont chargés correctement"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  try:
426
+ # Vérifier que tous les modèles sont disponibles
427
+ models_status = {
428
+ "analyse_medicale_model": pipeline is not None,
429
+ "analyze_risk_model": analyze_risk_model is not None,
430
+ "llm_model": llm_model is not None,
431
+ "llm_tokenizer": llm_tokenizer is not None
432
+ }
433
 
434
+ all_loaded = all(models_status.values())
435
+
436
+ return {
437
+ "status": "healthy" if all_loaded else "unhealthy",
438
+ "models_loaded": models_status,
439
+ "message": "Tous les modèles sont chargés" if all_loaded else "Certains modèles ne sont pas chargés"
440
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
  except Exception as e:
442
+ return {
443
+ "status": "error",
444
+ "error": str(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  }
 
446
 
447
  @app.post("/predict", response_model=PredictionResult)
448
  def predict(data: InputData):
 
 
 
 
449
  df = pd.DataFrame([data.dict()])
450
  preds = pipeline.predict(df)[0]
451
  return PredictionResult(
 
458
 
459
  @app.post("/upload-pdf", response_model=List[PredictionResult])
460
  async def upload_file(file: UploadFile = File(...)):
 
 
 
 
461
  content = await file.read()
462
  file_extension = file.filename.split('.')[-1].lower()
463
 
 
466
  if file.content_type != "application/pdf":
467
  raise HTTPException(status_code=400, detail="Le fichier doit être au format PDF")
468
 
469
+ # Traitement PDF existant
470
  extracted_text = extract_text_from_pdf_bytes(content)
471
  cleaned_text = nettoyer_text(extracted_text)
472
  patient_info = extract_patient_info(cleaned_text)
 
529
 
530
  @app.post("/analyze-risk")
531
  def analyze_risk(param: dict = Body(...)):
532
+ import pandas as pd
533
+ import numpy as np
534
+ # Utiliser le modèle globalement chargé
535
+ model = analyze_risk_model
536
+
537
  # Préparer le DataFrame à partir du paramètre reçu
538
  df_test = pd.DataFrame([param])
539
 
540
+ # Préparation des features dérivées (copie de preparer_features)
541
  df_result = df_test.copy()
542
  try:
543
  df_result['ValeurAnterieure'] = pd.to_numeric(df_result['ValeurAnterieure'], errors='coerce')
 
579
  features_for_ml = df_result[['DeltaValeurPrecedente', 'RatioValeurPrecedente',
580
  'PourcentageValeurMin', 'PourcentageValeurMax',
581
  'EcartNormalise', 'ValeurActuelle', 'CodeParametre']]
582
+ predicted_risk_num = model.predict(features_for_ml)[0]
583
  risk_map = {0: 'Aucun', 1: 'Faible', 2: 'Modéré', 3: 'Élevé'}
584
  degre_risque = risk_map.get(int(predicted_risk_num), 'Inconnu')
585
 
 
618
  "conseil": to_native(conseil)
619
  }
620
 
621
+ # Fonction de debug temporaire pour tester l'extraction
622
+ def debug_extraction(line):
623
+ """Teste l'extraction d'une ligne et affiche les résultats"""
624
+ match = re.search(REGEX_PARAMETRE, line)
625
+ if match:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
626
  return {
627
+ "param": match.group(1).strip(),
628
+ "valeur": match.group(2).strip(),
629
+ "unite": match.group(3).strip() if match.group(3) else "",
630
+ "valeur_ant": match.group(4).strip() if match.group(4) else None,
631
+ "date": match.group(5).strip() if match.group(5) else "",
632
+ "usuelles": match.group(6).strip() if match.group(6) else ""
633
  }
634
+ return None
635
+
636
+ # Ajouter les fonctions de traitement XML
637
+ def extract_valeurs_usuelles_xml(val):
638
+ """Extrait les bornes min/max des valeurs usuelles depuis un format XML."""
639
+ if not isinstance(val, str) or val.strip() == "":
640
+ return None, None
641
+ val = val.lower().replace(',', '.').strip()
642
+
643
+ try:
644
+ if '-' in val:
645
+ parts = val.split('-')
646
+ return float(parts[0].strip()), float(parts[1].strip())
647
+ elif 'inf à' in val:
648
+ return None, float(re.sub(r"[^\d.]", "", val))
649
+ elif 'sup à' in val or '>' in val:
650
+ return float(re.sub(r"[^\d.]", "", val)), None
651
+ except:
652
+ return None, None
653
+
654
+ return None, None
655
+
656
+ def parse_xml_file(xml_bytes: bytes) -> list:
657
+ """Parse un fichier XML et retourne les résultats au format attendu par l'API."""
658
+ try:
659
+ # Utiliser BytesIO pour lire les bytes comme un fichier
660
+ tree = ET.parse(io.BytesIO(xml_bytes))
661
+ root = tree.getroot()
662
 
663
+ results = []
664
+
665
+ demande = root.find(".//Demande")
666
+ if demande is None:
667
+ raise HTTPException(status_code=400, detail="Format XML non reconnu: élément 'Demande' introuvable")
668
+
669
+ nom_patient = demande.findtext("NomPatient", "").strip()
670
+ prenom_patient = demande.findtext("PrenomPatient", "").strip()
671
+ patient_name = f"{nom_patient} {prenom_patient}".strip()
672
+ patient_name = nettoyer_nom_patient(patient_name)
673
+ medecin = demande.findtext("MedecinPrescripteur", "").strip()
674
+ date_analyse = demande.findtext("DateSaisie", "").strip()
675
+
676
+ # Convertir la date si nécessaire
677
+ if re.match(r'^\d{4}-\d{2}-\d{2}$', date_analyse):
678
+ parts = date_analyse.split('-')
679
+ date_analyse = f"{parts[2]}/{parts[1]}/{parts[0]}"
680
+
681
+ for examen in demande.findall(".//Examen"):
682
+ famille = examen.findtext("Famille", "").strip()
683
+ code_analyse = examen.findtext("CodeAnalyse", "").strip()
684
+ lib_analyse = examen.findtext("LibAnalyse", "").strip()
685
+
686
+ for res in examen.findall("Resultat"):
687
+ cod_param = res.findtext("CodParametre", "").strip()
688
+ lib_param = res.findtext("LibParametre", "").strip()
689
+ valeur = res.findtext("Valeur", "").replace(",", ".").strip()
690
+ unite = res.findtext("Unite", "").strip()
691
+ val_usuelle = res.findtext("ValeurUsuelles", "").strip()
692
+
693
+ val_min, val_max = extract_valeurs_usuelles_xml(val_usuelle)
694
+
695
+ # Normalisation des valeurs
696
+ try:
697
+ valeur_actuelle = normalize_numeric_values(valeur)
698
+ except ValueError:
699
+ valeur_actuelle = ''
700
 
701
+ results.append({
702
+ "CodeParametre": cod_param.lower(),
703
+ "ValeurActuelle": valeur_actuelle,
704
+ "Unite": unite,
705
+ "ValeursUsuelles": val_usuelle,
706
+ "ValeurUsuelleMin": val_min,
707
+ "ValeurUsuelleMax": val_max,
708
+ "ValeurAnterieure": None,
709
+ "DateAnterieure": '',
710
+ "NomPatient": patient_name,
711
+ "Medecin": medecin,
712
+ "DateAnalyse": date_analyse,
713
+ "CodParametre": cod_param, # Champ prédit (copie du code paramètre)
714
+ "LIBMEDWINabrege": cod_param, # Pourrait être différent, dépend du modèle
715
+ "LibParametre": lib_param,
716
+ "FAMILLE": famille
717
+ })
718
+
719
+ if not results:
720
+ raise HTTPException(status_code=400, detail="Aucun paramètre reconnu dans le XML")
721
+
722
+ return results
723
+ except Exception as e:
724
+ raise HTTPException(status_code=400, detail=f"Erreur lors du traitement du XML: {str(e)}")
requirements.txt CHANGED
@@ -11,4 +11,5 @@ scikit-learn==1.3.2
11
  transformers==4.36.2
12
  torch==2.1.2
13
  python-dotenv==1.0.0
14
- huggingface-hub==0.20.3
 
 
11
  transformers==4.36.2
12
  torch==2.1.2
13
  python-dotenv==1.0.0
14
+ huggingface-hub==0.20.3
15
+ requests==2.31.0