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

first try

Browse files
Files changed (5) hide show
  1. .gitignore +131 -0
  2. Dockerfile +16 -0
  3. README.md +190 -4
  4. app.py +873 -0
  5. requirements.txt +14 -0
.gitignore ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+ MANIFEST
23
+
24
+ # PyInstaller
25
+ *.manifest
26
+ *.spec
27
+
28
+ # Installer logs
29
+ pip-log.txt
30
+ pip-delete-this-directory.txt
31
+
32
+ # Unit test / coverage reports
33
+ htmlcov/
34
+ .tox/
35
+ .coverage
36
+ .coverage.*
37
+ .cache
38
+ nosetests.xml
39
+ coverage.xml
40
+ *.cover
41
+ .hypothesis/
42
+ .pytest_cache/
43
+
44
+ # Translations
45
+ *.mo
46
+ *.pot
47
+
48
+ # Django stuff:
49
+ *.log
50
+ local_settings.py
51
+ db.sqlite3
52
+
53
+ # Flask stuff:
54
+ instance/
55
+ .webassets-cache
56
+
57
+ # Scrapy stuff:
58
+ .scrapy
59
+
60
+ # Sphinx documentation
61
+ docs/_build/
62
+
63
+ # PyBuilder
64
+ target/
65
+
66
+ # Jupyter Notebook
67
+ .ipynb_checkpoints
68
+
69
+ # pyenv
70
+ .python-version
71
+
72
+ # celery beat schedule file
73
+ celerybeat-schedule
74
+
75
+ # SageMath parsed files
76
+ *.sage.py
77
+
78
+ # Environments
79
+ .env
80
+ .venv
81
+ env/
82
+ venv/
83
+ ENV/
84
+ env.bak/
85
+ venv.bak/
86
+
87
+ # Spyder project settings
88
+ .spyderproject
89
+ .spyproject
90
+
91
+ # Rope project settings
92
+ .ropeproject
93
+
94
+ # mkdocs documentation
95
+ /site
96
+
97
+ # mypy
98
+ .mypy_cache/
99
+ .dmypy.json
100
+ dmypy.json
101
+
102
+ # IDE
103
+ .vscode/
104
+ .idea/
105
+ *.swp
106
+ *.swo
107
+ *~
108
+
109
+ # OS
110
+ .DS_Store
111
+ .DS_Store?
112
+ ._*
113
+ .Spotlight-V100
114
+ .Trashes
115
+ ehthumbs.db
116
+ Thumbs.db
117
+
118
+ # Temporary files
119
+ *.tmp
120
+ *.temp
121
+ *.log
122
+
123
+ # Model files (if any are downloaded locally)
124
+ *.joblib
125
+ *.pkl
126
+ *.pth
127
+ *.pt
128
+
129
+ # Test files
130
+ test_*.py
131
+ *_test.py
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # you will also find guides on how best to write your Dockerfile
3
+
4
+ FROM python:3.9
5
+
6
+ RUN useradd -m -u 1000 user
7
+ USER user
8
+ ENV PATH="/home/user/.local/bin:$PATH"
9
+
10
+ WORKDIR /app
11
+
12
+ COPY --chown=user ./requirements.txt requirements.txt
13
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
+
15
+ COPY --chown=user . /app
16
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,196 @@
1
  ---
2
  title: MedWin Analyzer
3
- emoji: 👀
4
- colorFrom: yellow
5
- colorTo: red
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: MedWin Analyzer
3
+ emoji: 🏥
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
  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**
app.py ADDED
@@ -0,0 +1,873 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Body
2
+ from pydantic import BaseModel
3
+ import joblib
4
+ import pandas as pd
5
+ import pdfplumber
6
+ import re
7
+ import numpy as np
8
+ import io
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from typing import List, Dict, Any, Optional
11
+ import os
12
+ import json
13
+ from rapidfuzz import process
14
+ from sklearn.impute import SimpleImputer
15
+ import random
16
+ import math
17
+ 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=["*"],
41
+ )
42
+
43
+ class NumericConverter(BaseEstimator, TransformerMixin):
44
+ """
45
+ Transformer that converts string values to numeric, with special handling for 'Négatif'
46
+ """
47
+ def __init__(self, negatif_value=0):
48
+ self.negatif_value = negatif_value
49
+
50
+ def fit(self, X, y=None):
51
+ return self
52
+
53
+ def transform(self, X):
54
+ X_copy = X.copy()
55
+ for col in X_copy.columns:
56
+ mask = X_copy[col] == 'Négatif'
57
+ if mask.any():
58
+ X_copy.loc[mask, col] = self.negatif_value
59
+ X_copy[col] = pd.to_numeric(X_copy[col], errors='coerce')
60
+ X_copy.fillna(0, inplace=True)
61
+ return X_copy
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)
125
+
126
+ # ==== Pydantic Models ====
127
+ class InputData(BaseModel):
128
+ CodeParametre: str
129
+ ValeurActuelle: float
130
+ Unite: str
131
+ ValeursUsuelles: str
132
+ ValeurUsuelleMin: float
133
+ ValeurUsuelleMax: float
134
+ ValeurAnterieure: Optional[float] = None
135
+ DateAnterieure: str = ''
136
+
137
+ class PredictionResult(BaseModel):
138
+ # Champs d'entrée
139
+ CodeParametre: str
140
+ ValeurActuelle: float
141
+ Unite: str
142
+ ValeursUsuelles: str
143
+ ValeurUsuelleMin: float
144
+ ValeurUsuelleMax: float
145
+ ValeurAnterieure: Optional[float] = None
146
+ DateAnterieure: str = ''
147
+ # Champs prédits
148
+ CodParametre: str
149
+ LIBMEDWINabrege: str
150
+ LibParametre: str
151
+ FAMILLE: str
152
+ # Informations du patient
153
+ NomPatient: str = "Patient inconnu"
154
+ Medecin: str = "Médecin inconnu"
155
+ DateAnalyse: str = ""
156
+
157
+ # ==== Constants ====
158
+ TYPE_ANALYSES = {
159
+ "hématologie": ["hematologie", "hématologie", "CYTO-HEMATOLOGIE", "HEMATOLOGIE"],
160
+ "biochimie": ["biochimie", "BIO-CHIMIE", "BIOCHIMIE"],
161
+ "enzymologie": ["enzymologie", "ENZYMOLOGIE"],
162
+ "hormonologie": ["hormonologie", "HORMONOLOGIE"],
163
+ "marqueurs biochimiques": ["marqueurs biochimiques"],
164
+ "biochimie des urines": ["biochimie des urines", "CHIMIE URINE"],
165
+ "immunologie": ["immunologie"],
166
+ "microbiologie": ["MICROBIOLOGIE"],
167
+ "coagulation": ["COAGULATION"],
168
+ "hémostase-coagulation": ["hemostase-coagulation"],
169
+ "hémostase": ["hemostase"],
170
+ "bactériologie": ["bacteriologie"],
171
+ "antibiogramme": ["antibiogramme"],
172
+ "biochimie clinique": ["biochimie clinique", "biochimie clinique (sang)"],
173
+ "vitamines": ["vitamines"],
174
+ "marqueurs tumoraux": ["marqueurs tumoraux"],
175
+ "marqueurs cardiaques": ["marqueurs cardiaques"],
176
+ "immuno-hématologie": ["immuno-hematologie"],
177
+ "hormones": ["hormones"],
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*(.*)"
185
+ REGEX_PARAMETRE = r"([\w\s]+?)[\s\.]+(\d+(?:[ \.,]\d+)*)\s*([a-zA-Z/%³]*)\s*(\d+(?:[ \.,]\d+)*)?\s*(\d{2}/\d{2}/\d{2,4})?\s*\(([^)]*)\)?"
186
+
187
+ # Unit mapping
188
+ UNIT_MAPPING = {
189
+ 'millions/mm³': '10^6/mm³',
190
+ 'millions/mm3': '10^6/mm³',
191
+ 'µ3': 'fl',
192
+ 'µl': 'fl',
193
+ 'ui/l': 'UI/L',
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):
201
+ """Associe un type d'analyse à partir du dictionnaire."""
202
+ texte = texte.lower()
203
+ for type_normalise, variantes in TYPE_ANALYSES.items():
204
+ if texte in [var.lower() for var in variantes]:
205
+ return type_normalise
206
+ return texte
207
+
208
+ def normalize_numeric_values(val):
209
+ """Normalise les valeurs numériques."""
210
+ if not isinstance(val, str):
211
+ return val
212
+
213
+ # Supprimer les espaces à l'intérieur des nombres (ex: 4 290 -> 4290)
214
+ val = re.sub(r'(?<=\d)\s+(?=\d)', '', val)
215
+
216
+ # Remplacer les virgules par des points pour la conversion
217
+ val = val.replace(',', '.')
218
+
219
+ if val.isdigit() and val.startswith('0') and len(val) > 1:
220
+ return int(val)
221
+ try:
222
+ return float(val)
223
+ except ValueError:
224
+ return val
225
+
226
+ def extract_min_max(valeur_usuelles):
227
+ """Extrait les bornes min/max des valeurs usuelles."""
228
+ if not isinstance(valeur_usuelles, str):
229
+ return None, None
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+)?)'
236
+ lt_pattern = r'<\s*(\d+(?:[.,]\d+)?)'
237
+ gt_pattern = r'>\s*(\d+(?:[.,]\d+)?)'
238
+
239
+ range_match = re.search(range_pattern, valeur_usuelles)
240
+ if range_match:
241
+ min_val = range_match.group(1).replace(',', '.')
242
+ max_val = range_match.group(2).replace(',', '.')
243
+ return float(min_val), float(max_val)
244
+
245
+ lt_match = re.search(lt_pattern, valeur_usuelles)
246
+ if lt_match:
247
+ return None, float(lt_match.group(1).replace(',', '.'))
248
+
249
+ gt_match = re.search(gt_pattern, valeur_usuelles)
250
+ if gt_match:
251
+ return float(gt_match.group(1).replace(',', '.')), None
252
+
253
+ return None, None
254
+
255
+ def nettoyer_code_parametre(code_param):
256
+ """Nettoie le code paramètre."""
257
+ if pd.isna(code_param):
258
+ return code_param
259
+ code_param = str(code_param)
260
+ code_param = re.sub(r'\s+\d+[.,]?\d*\s*\S*$', '', code_param)
261
+ return code_param.strip().lower()
262
+
263
+ def extract_text_from_pdf_bytes(pdf_bytes: bytes) -> str:
264
+ """Extrait le texte d'un PDF à partir de son contenu en bytes."""
265
+ extracted_text = ""
266
+ with pdfplumber.open(io.BytesIO(pdf_bytes)) as pdf:
267
+ for page in pdf.pages:
268
+ text = page.extract_text()
269
+ if text:
270
+ extracted_text += text + "\n"
271
+ return extracted_text
272
+
273
+ def nettoyer_text(contenu: str) -> str:
274
+ """Nettoyage avancé du texte."""
275
+ contenu = re.sub(r'(\b[A-ZÀ-ÖØ-öø-ÿ])(?:\.+[A-ZÀ-ÖØ-öø-ÿ])+\b', lambda m: m.group(0).replace('.', ''), contenu)
276
+ contenu = re.sub(r'\.(?!\d)', '', contenu)
277
+ return contenu
278
+
279
+ def nettoyer_nom_patient(nom):
280
+ """Supprime les caractères non nécessaires comme les astérisques du nom du patient."""
281
+ if not nom or not isinstance(nom, str):
282
+ return "Patient inconnu"
283
+ # Supprimer les astérisques
284
+ nom = re.sub(r'[*]+', '', nom)
285
+ # Supprimer les espaces multiples et les espaces au début/fin
286
+ nom = re.sub(r'\s+', ' ', nom).strip()
287
+ return nom if nom else "Patient inconnu"
288
+
289
+ def extract_patient_info(text: str) -> Dict[str, str]:
290
+ """Extrait les informations du patient."""
291
+ patient_info = {
292
+ "NomPatient": "Patient inconnu",
293
+ "Medecin": "Médecin inconnu",
294
+ "DateAnalyse": ""
295
+ }
296
+
297
+ date_match = re.search(REGEX_DATE, text)
298
+ if date_match:
299
+ patient_info["DateAnalyse"] = date_match.group(1)
300
+
301
+ patient_match = re.search(REGEX_PATIENT, text)
302
+ if patient_match:
303
+ patient_info["NomPatient"] = nettoyer_nom_patient(patient_match.group(1).strip())
304
+
305
+ medecin_match = re.search(REGEX_MEDECIN, text)
306
+ if medecin_match:
307
+ patient_info["Medecin"] = medecin_match.group(1).strip()
308
+
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:
316
+ line = line.strip()
317
+ if not line:
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()
324
+ values_part = soit_match.group(3).strip()
325
+ line = f"{param_name} {values_part}"
326
+
327
+ param_match = re.search(REGEX_PARAMETRE, line)
328
+ valeur_anterieure = None
329
+ date_anterieure = ''
330
+ if param_match:
331
+ parametre = param_match.group(1).replace('.', '').strip().lower()
332
+ valeur_actuelle = param_match.group(2).strip()
333
+ unite = param_match.group(3).strip()
334
+ valeur_usuelles = param_match.group(6).strip() if param_match.group(6) else ""
335
+ valeur_anterieure = param_match.group(4).strip() if param_match.group(4) else None
336
+ date_anterieure = param_match.group(5).strip() if param_match.group(5) else ''
337
+ # Normalisation des unités
338
+ unite = UNIT_MAPPING.get(unite.lower(), unite)
339
+ # Normalisation des valeurs numériques
340
+ try:
341
+ valeur_actuelle = normalize_numeric_values(valeur_actuelle)
342
+ except ValueError:
343
+ valeur_actuelle = ''
344
+ try:
345
+ valeur_anterieure = normalize_numeric_values(valeur_anterieure) if valeur_anterieure not in [None, ''] else None
346
+ except ValueError:
347
+ valeur_anterieure = None
348
+ # Extraction des bornes min/max
349
+ min_val, max_val = extract_min_max(valeur_usuelles)
350
+ # Nettoyage du code paramètre
351
+ parametre = nettoyer_code_parametre(parametre)
352
+ results.append({
353
+ "CodeParametre": parametre,
354
+ "ValeurActuelle": valeur_actuelle,
355
+ "Unite": unite,
356
+ "ValeursUsuelles": valeur_usuelles,
357
+ "ValeurUsuelleMin": min_val,
358
+ "ValeurUsuelleMax": max_val,
359
+ "ValeurAnterieure": valeur_anterieure,
360
+ "DateAnterieure": date_anterieure
361
+ })
362
+ if not results:
363
+ raise HTTPException(status_code=400, detail="Aucun paramètre reconnu dans le PDF")
364
+ return results
365
+
366
+ def handle_missing_values(df: pd.DataFrame) -> pd.DataFrame:
367
+ """Gère les valeurs manquantes dans le DataFrame."""
368
+ # Remplir les valeurs manquantes pour les informations du patient
369
+ df['NomPatient'] = df['NomPatient'].fillna('patient')
370
+ df['Medecin'] = df['Medecin'].fillna('médecin')
371
+ df['DateAnalyse'] = df['DateAnalyse'].fillna('')
372
+
373
+ # Remplir les valeurs manquantes pour les paramètres
374
+ df['Unite'] = df['Unite'].fillna('-')
375
+ df['ValeursUsuelles'] = df['ValeursUsuelles'].fillna('')
376
+
377
+ # Remplir les valeurs manquantes pour les valeurs numériques
378
+ df['ValeurUsuelleMin'] = df['ValeurUsuelleMin'].fillna(-1e6)
379
+ df['ValeurUsuelleMax'] = df['ValeurUsuelleMax'].fillna(1e6)
380
+ df['ValeurActuelle'] = df['ValeurActuelle'].fillna(0)
381
+
382
+ # Pour les codes paramètres manquants, utiliser une valeur par défaut
383
+ df['CodeParametre'] = df['CodeParametre'].fillna('parametre_inconnu')
384
+
385
+ # Convertir explicitement les colonnes numériques en float
386
+ numeric_columns = ['ValeurActuelle', 'ValeurUsuelleMin', 'ValeurUsuelleMax']
387
+ for col in numeric_columns:
388
+ df[col] = pd.to_numeric(df[col], errors='coerce').fillna(0).astype(float)
389
+
390
+ return df
391
+
392
+ def postprocess_valeurs_usuelles(df):
393
+ def format_valeurs(row):
394
+ val_usuelle = str(row['ValeursUsuelles'])
395
+ # nombre - 1,000,000
396
+ match_min = re.match(r'^\s*(\d+(?:[.,]?\d+)?)\s*-\s*1[,.]?0{6,}', val_usuelle)
397
+ if match_min:
398
+ min_val = match_min.group(1).replace(',', '.')
399
+ row['ValeursUsuelles'] = f"{min_val}<"
400
+ row['ValeurUsuelleMin'] = float(min_val)
401
+ row['ValeurUsuelleMax'] = ''
402
+ return row
403
+ # 1,000,000 - nombre
404
+ match_max = re.match(r'^1[,.]?0{6,}\s*-\s*(\d+(?:[.,]?\d+)?)', val_usuelle)
405
+ if match_max:
406
+ max_val = match_max.group(1).replace(',', '.')
407
+ row['ValeursUsuelles'] = f"{max_val}>"
408
+ row['ValeurUsuelleMin'] = ''
409
+ row['ValeurUsuelleMax'] = float(max_val)
410
+ return row
411
+ # -1,000,000
412
+ match_neg = re.match(r'^-1[,.]?0{6,}\s*-\s*(\d+(?:[.,]?\d+)?)', val_usuelle)
413
+ if match_neg:
414
+ nombre = match_neg.group(1).replace(',', '.')
415
+ row['ValeurUsuelleMin'] = ''
416
+ row['ValeursUsuelles'] = f"{nombre}>"
417
+ row['ValeurUsuelleMax'] = float(nombre)
418
+ return row
419
+ return row
420
+ df = df.apply(format_valeurs, axis=1)
421
+ return df
422
+
423
+ def clean_json_value(val, field_type):
424
+ if isinstance(val, float) and (math.isnan(val) or math.isinf(val)):
425
+ if field_type == float or field_type == Optional[float]:
426
+ return None
427
+ else:
428
+ return "n'existe pas"
429
+ return val
430
+
431
+ def to_native(val):
432
+ if isinstance(val, (np.generic,)):
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(
597
+ **data.dict(),
598
+ CodParametre=preds[0],
599
+ LIBMEDWINabrege=preds[1],
600
+ LibParametre=preds[2],
601
+ FAMILLE=preds[3]
602
+ )
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
+
613
+ try:
614
+ if file_extension == "pdf":
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)
622
+ data_fields_list = extract_all_fields_from_text(cleaned_text)
623
+
624
+ for item in data_fields_list:
625
+ item.update(patient_info)
626
+
627
+ # Créer le DataFrame
628
+ df = pd.DataFrame(data_fields_list)
629
+
630
+ elif file_extension == "xml":
631
+ # Traitement XML
632
+ results = parse_xml_file(content)
633
+ # Convertir la liste de résultats en DataFrame
634
+ df = pd.DataFrame(results)
635
+
636
+ else:
637
+ raise HTTPException(status_code=400, detail="Format de fichier non pris en charge. Utilisez PDF ou XML.")
638
+
639
+ # Traitement commun pour PDF et XML
640
+ df = handle_missing_values(df)
641
+ df = postprocess_valeurs_usuelles(df)
642
+
643
+ # Si on a du PDF et qu'on a besoin de prédire les paramètres
644
+ if file_extension == "pdf":
645
+ # Faire la prédiction
646
+ preds = pipeline.predict(df)
647
+
648
+ # Créer les résultats avec les prédictions
649
+ results = []
650
+ for input_data, p in zip(df.to_dict('records'), preds):
651
+ # Nettoyer les valeurs NaN/inf selon le type attendu
652
+ for k, v in input_data.items():
653
+ field_type = PredictionResult.model_fields[k].annotation if k in PredictionResult.model_fields else str
654
+ input_data[k] = clean_json_value(v, field_type)
655
+ result = PredictionResult(
656
+ **input_data,
657
+ CodParametre=p[0],
658
+ LIBMEDWINabrege=p[1],
659
+ LibParametre=p[2],
660
+ FAMILLE=p[3]
661
+ )
662
+ results.append(result)
663
+ else:
664
+ # Pour XML, nous avons déjà les informations complètes
665
+ results = []
666
+ for input_data in df.to_dict('records'):
667
+ # Nettoyer les valeurs NaN/inf selon le type attendu
668
+ for k, v in input_data.items():
669
+ field_type = PredictionResult.model_fields[k].annotation if k in PredictionResult.model_fields else str
670
+ input_data[k] = clean_json_value(v, field_type)
671
+ result = PredictionResult(**input_data)
672
+ results.append(result)
673
+
674
+ return results
675
+
676
+ except Exception as e:
677
+ raise HTTPException(status_code=500, detail=f"Erreur lors du traitement: {str(e)}")
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')
692
+ for i, row in df_result.iterrows():
693
+ if not pd.isna(row['ValeurAnterieure']) and row['ValeurAnterieure'] != 0:
694
+ df_result.loc[i, 'DeltaValeurPrecedente'] = row['ValeurActuelle'] - row['ValeurAnterieure']
695
+ df_result.loc[i, 'RatioValeurPrecedente'] = row['ValeurActuelle'] / row['ValeurAnterieure']
696
+ else:
697
+ df_result.loc[i, 'DeltaValeurPrecedente'] = 0
698
+ df_result.loc[i, 'RatioValeurPrecedente'] = 1
699
+ except:
700
+ df_result['DeltaValeurPrecedente'] = 0
701
+ df_result['RatioValeurPrecedente'] = 1
702
+ df_result['PourcentageValeurMin'] = (df_result['ValeurActuelle'] / df_result['ValeurUsuelleMin']) * 100
703
+ df_result['PourcentageValeurMax'] = (df_result['ValeurActuelle'] / df_result['ValeurUsuelleMax']) * 100
704
+ df_result['EcartNormalise'] = 0.0
705
+ mask = (df_result['ValeurUsuelleMax'] - df_result['ValeurUsuelleMin']) > 0
706
+ df_result.loc[mask, 'EcartNormalise'] = (
707
+ (df_result.loc[mask, 'ValeurActuelle'] - df_result.loc[mask, 'ValeurUsuelleMin']) /
708
+ (df_result.loc[mask, 'ValeurUsuelleMax'] - df_result.loc[mask, 'ValeurUsuelleMin'])
709
+ )
710
+ for col in ['DeltaValeurPrecedente', 'RatioValeurPrecedente', 'PourcentageValeurMin',
711
+ 'PourcentageValeurMax', 'EcartNormalise']:
712
+ df_result[col] = df_result[col].replace([np.inf, -np.inf], np.nan)
713
+ df_result[col] = df_result[col].fillna(0)
714
+
715
+ # Statut
716
+ valeur_actuelle = df_result['ValeurActuelle'].values[0]
717
+ min_usuel = df_result['ValeurUsuelleMin'].values[0]
718
+ max_usuel = df_result['ValeurUsuelleMax'].values[0]
719
+ if valeur_actuelle < min_usuel:
720
+ statut = "BAS"
721
+ elif valeur_actuelle > max_usuel:
722
+ statut = "ÉLEVÉ"
723
+ else:
724
+ statut = "NORMAL"
725
+
726
+ # Features pour le ML
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
+
734
+ # Détermination de la tendance
735
+ valeur_anterieure = df_result['ValeurAnterieure'].values[0]
736
+ tendance = "Indéterminée (pas de valeur antérieure)"
737
+ if not pd.isna(valeur_anterieure) and valeur_anterieure != 0:
738
+ delta = df_result['DeltaValeurPrecedente'].values[0]
739
+ if abs(delta) < 0.05 * (max_usuel - min_usuel):
740
+ tendance = "Stable"
741
+ elif delta > 0:
742
+ tendance = "En hausse"
743
+ else:
744
+ tendance = "En baisse"
745
+
746
+ # Conseil simple
747
+ if degre_risque == "Aucun":
748
+ conseil = "Aucune action particulière requise. Les valeurs sont dans la plage normale."
749
+ elif degre_risque == "Faible":
750
+ conseil = f"À surveiller lors du prochain contrôle. Le {param['CodParametre']} est légèrement {statut.lower()}."
751
+ elif degre_risque == "Modéré":
752
+ conseil = f"Surveillance recommandée. Le {param['CodParametre']} est {statut.lower()} avec un risque modéré."
753
+ else: # Élevé
754
+ conseil = f"Consultation médicale recommandée. Le {param['CodParametre']} présente un risque élevé."
755
+
756
+ # Conversion explicite des types pour la réponse JSON
757
+ return {
758
+ "parametre": to_native(param['CodeParametre']),
759
+ "valeur_actuelle": to_native(valeur_actuelle),
760
+ "unite": to_native(param.get('Unite', '')),
761
+ "valeur_anterieure": to_native(valeur_anterieure) if not pd.isna(valeur_anterieure) else "n'existe pas",
762
+ "valeurs_usuelles": to_native(param.get('ValeursUsuelles', '')),
763
+ "statut_risque": to_native(statut),
764
+ "degre_risque": to_native(degre_risque),
765
+ "tendance": to_native(tendance),
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
+ )
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn[standard]==0.24.0
3
+ pydantic==2.5.0
4
+ joblib==1.3.2
5
+ pandas==2.1.4
6
+ pdfplumber==0.10.3
7
+ numpy==1.24.3
8
+ python-multipart==0.0.6
9
+ rapidfuzz==3.6.1
10
+ 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