rkonan commited on
Commit
f725085
·
1 Parent(s): 95a6ecf

first commit

Browse files
.dockerignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .DS_Store
5
+ .env
.gitattributes CHANGED
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ model/ filter=lfs diff=lfs merge=lfs -text
37
+ model/best_efficientnetv2m_gradcam.keras filter=lfs diff=lfs merge=lfs -text
38
+ model/best_ResNet50V2_04_improved_target_augment.keras filter=lfs diff=lfs merge=lfs -text
39
+ model/best_ResNet50V2_gradcam.keras filter=lfs diff=lfs merge=lfs -text
40
+ model/best_EfficientNetV2M_03_improved_global_augment.keras filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Environnements virtuels
2
+ venv/
3
+ .venv/
4
+ env/
5
+ .env/
6
+
7
+ # Fichiers Python compilés
8
+ app/__pycache__/
9
+ *.pyc
10
+
11
+ # Fichiers système
12
+ .DS_Store
13
+
14
+ # Configurations IDE
15
+ .vscode/
16
+ .idea/
17
+
18
+ # Fichiers de logs
19
+ *.log
Dockerfile ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Image TensorFlow GPU (fonctionne aussi en CPU-only)
2
+ FROM tensorflow/tensorflow:2.15.0-gpu
3
+
4
+ # Installation des dépendances système supplémentaires
5
+ RUN apt-get update && apt-get install -y \
6
+ libgl1-mesa-glx \
7
+ libglib2.0-0 \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Création de l'utilisateur user (requis par Hugging Face)
11
+ RUN useradd -m -u 1000 user
12
+ ENV HOME=/home/user
13
+ ENV PATH=/home/user/.local/bin:$PATH
14
+
15
+ # Définir le répertoire de travail
16
+ WORKDIR $HOME/app
17
+
18
+ # Copier les fichiers de requirements avec les bonnes permissions
19
+ COPY --chown=user:user requirements.txt .
20
+
21
+ # Passer à l'utilisateur user
22
+ USER user
23
+
24
+ # Installer les dépendances Python
25
+ RUN pip install --no-cache-dir --upgrade pip && \
26
+ pip install --no-cache-dir -r requirements.txt
27
+
28
+ # Copier le reste de l'application
29
+ COPY --chown=user:user . .
30
+
31
+ # Port requis par Hugging Face Spaces
32
+ EXPOSE 7860
33
+
34
+ # Variables d'environnement pour Hugging Face Spaces
35
+ ENV PORT=7860
36
+ ENV HOST=0.0.0.0
37
+
38
+ # Commande de démarrage compatible HF Spaces
39
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860", "--log-level", "info"]
README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
  title: Reco Efficientnet Api
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: Reco Efficientnet Api
3
+ emoji: 🏆
4
+ colorFrom: red
5
+ colorTo: gray
6
  sdk: docker
7
  pinned: false
8
+ short_description: Reco EfficientNet API
9
  ---
10
 
11
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
12
+
13
+ export API_ENV=local && uvicorn app.main:app --host 0.0.0.0 --port 7861 --log-level debug
app/config.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ ENV = os.getenv("API_ENV", "space")
4
+ MODEL_NAME = os.getenv("MODEL_NAME", "EfficientNetV2M")
5
+ MODEL_TYPE =os.getenv("MODEL_TYPE", "CNN")
6
+
7
+ if ENV == "local":
8
+ ORCHESTRATOR_URL = "http://0.0.0.0:7860"
9
+ OWN_URL = "http://0.0.0.0:7861"
10
+ else:
11
+ ORCHESTRATOR_URL = "https://rkonan-reco-orchestrator-api.hf.space"
12
+ OWN_URL = "https://rkonan-reco-efficientnet-api.hf.space"
app/log.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # log.py
2
+ import logging
3
+
4
+ logger = logging.getLogger("model_api")
5
+ logger.setLevel(logging.DEBUG)
6
+
7
+ formatter = logging.Formatter(
8
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
9
+ )
10
+
11
+ console_handler = logging.StreamHandler()
12
+ console_handler.setFormatter(formatter)
13
+
14
+ if not logger.handlers:
15
+ logger.addHandler(console_handler)
app/main.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #main.py
2
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Request,Query
3
+
4
+ from pydantic import BaseModel
5
+ from typing import Union
6
+ import base64
7
+
8
+ from app.model import load_model, predict_with_model
9
+ import os
10
+ import threading
11
+ import time
12
+ from app.utils import heartbeat,register_forever
13
+ from app.log import logger
14
+ from app.config import MODEL_NAME, ENV,MODEL_TYPE
15
+
16
+
17
+
18
+ logger.info(f"ENV :{ENV}")
19
+
20
+ app = FastAPI()
21
+
22
+
23
+
24
+ def load_models_once():
25
+ _ = load_model()
26
+
27
+ @app.on_event("startup")
28
+ def startup():
29
+ load_models_once()
30
+ threading.Thread(target=register_forever, daemon=True).start()
31
+ threading.Thread(target=heartbeat, daemon=True).start()
32
+
33
+
34
+ class ImagePayload(BaseModel):
35
+ image: str # chaîne encodée en base64
36
+
37
+
38
+ @app.post("/predict")
39
+ async def predict(request: Request,
40
+ file: UploadFile = File(None),
41
+ payload: Union[ImagePayload, None] = None,
42
+ show_heatmap: bool = Query(False, description="Afficher la heatmap"),
43
+ ):
44
+
45
+ logger.info("🔁 Requête reçue")
46
+ logger.info(f"✅ Show heatmap : {show_heatmap}")
47
+
48
+ try:
49
+ # Cas 1 : multipart avec fichier
50
+ if file is not None:
51
+ image_bytes = await file.read()
52
+ logger.debug(f"✅ Image reçue via multipart : {file.filename} — {len(image_bytes)} octets")
53
+
54
+ # Cas 2 : JSON base64
55
+ elif payload is not None:
56
+ image_bytes = base64.b64decode(payload.image)
57
+ logger.debug(f"✅ Image décodée depuis base64 : {len(image_bytes)} octets)")
58
+
59
+ else:
60
+ logger.info("⚠️ Aucune image reçue")
61
+ raise HTTPException(status_code=400, detail="Format de requête non supporté.")
62
+
63
+ # Appel de ta logique de prédiction
64
+ logger.debug("🔍 Appel du vote multi-modèles...")
65
+ models = load_model()
66
+ if not models:
67
+ raise HTTPException(status_code=500, detail="Aucun modèle chargé.")
68
+ model_config = models[0]
69
+ prediction = predict_with_model(model_config, image_bytes, show_heatmap)
70
+
71
+ # Pour l’instant : réponse simulée
72
+ return prediction
73
+
74
+ except Exception as e:
75
+ logger.error("❌ Une erreur s'est produite", exc_info=True)
76
+ raise HTTPException(status_code=500, detail=str(e))
77
+
78
+
79
+ @app.get("/health")
80
+ def health_check():
81
+ return {
82
+ "status": "ok",
83
+ "model_name": MODEL_NAME,
84
+ "model_type":MODEL_TYPE,
85
+ "timestamp": time.time()
86
+ }
app/main_old.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Request,Query
2
+ import io
3
+ from PIL import Image
4
+ from io import BytesIO
5
+ from pydantic import BaseModel
6
+ from typing import Union
7
+ from io import BytesIO
8
+ import base64
9
+ import logging
10
+ import logging
11
+ from app.model import load_model, predict_with_model
12
+ # Configuration de base du logger
13
+ logging.basicConfig(
14
+ level=logging.DEBUG, # DEBUG pour voir tous les logs (INFO, WARNING, ERROR, etc.)
15
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+
22
+ app = FastAPI()
23
+
24
+
25
+ @app.on_event("startup")
26
+ def load_models_once():
27
+ _ = load_model()
28
+
29
+ class ImagePayload(BaseModel):
30
+ image: str # chaîne encodée en base64
31
+ @app.post("/predict")
32
+ async def predict(request: Request,
33
+ file: UploadFile = File(None),
34
+ payload: Union[ImagePayload, None] = None,
35
+ show_heatmap: bool = Query(False, description="Afficher la heatmap"),
36
+ ):
37
+
38
+ logger.info("🔁 Requête reçue")
39
+ logger.info(f"✅ Show heatmap : {show_heatmap}")
40
+
41
+ try:
42
+ # Cas 1 : multipart avec fichier
43
+ if file is not None:
44
+ image_bytes = await file.read()
45
+ logger.debug("✅ Image reçue via multipart :", file.filename, len(image_bytes), "octets")
46
+
47
+ # Cas 2 : JSON base64
48
+ elif await request.json():
49
+ body = await request.json()
50
+ if "image" not in body:
51
+ raise HTTPException(status_code=422, detail="Champ 'image' manquant.")
52
+ image_base64 = body["image"]
53
+ image_bytes = base64.b64decode(image_base64)
54
+ logger.debug("✅ Image décodée depuis base64 :", len(image_bytes), "octets")
55
+
56
+ else:
57
+ logger.info("⚠️ Aucune image reçue")
58
+ raise HTTPException(status_code=400, detail="Format de requête non supporté.")
59
+
60
+ # Appel de ta logique de prédiction
61
+ logger.debug("🔍 Appel du vote multi-modèles...")
62
+ model_config = load_model()[0]
63
+ prediction = predict_with_model(model_config, image_bytes, show_heatmap)
64
+
65
+ # Pour l’instant : réponse simulée
66
+ return prediction
67
+
68
+ except Exception as e:
69
+ logger.error("❌ Une erreur s'est produite", exc_info=True)
70
+ raise HTTPException(status_code=500, detail=str(e))
71
+
72
+
73
+ @app.get("/health")
74
+ def health_check():
75
+ return {"status": "ok"}
app/model.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tensorflow as tf
2
+ import numpy as np
3
+ from PIL import Image
4
+ import tensorflow as tf
5
+ import logging
6
+ import numpy as np
7
+ from PIL import Image
8
+ from keras.applications.efficientnet_v2 import preprocess_input as effnet_preprocess
9
+ from keras.applications.resnet_v2 import preprocess_input as resnet_preprocess
10
+ import io
11
+ from tf_keras_vis.gradcam import Gradcam,GradcamPlusPlus
12
+ from tf_keras_vis.utils import normalize
13
+
14
+ import numpy as np
15
+ import tensorflow as tf
16
+ from tf_keras_vis.saliency import Saliency
17
+ from tf_keras_vis.utils import normalize
18
+ import numpy as np
19
+ import tensorflow as tf
20
+ from tf_keras_vis.saliency import Saliency
21
+ from tf_keras_vis.utils import normalize
22
+ import logging
23
+ import time
24
+
25
+ from typing import TypedDict, Callable, Any
26
+ logging.basicConfig(
27
+ level=logging.INFO, # ou logging.DEBUG
28
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
29
+ )
30
+ logger = logging.getLogger(__name__)
31
+ confidence_threshold=0.55
32
+ entropy_threshold=2
33
+
34
+ class ModelStruct(TypedDict):
35
+ model_name: str
36
+ model: tf.keras.Model
37
+ gradcam_model:tf.keras.Model
38
+ preprocess_input: Callable[[np.ndarray], Any]
39
+ target_size: tuple[int, int]
40
+ last_conv_layer:str
41
+ gradcam_type:str
42
+
43
+
44
+ _model_cache: list[ModelStruct] | None = None
45
+ def load_model() -> list[ModelStruct]:
46
+ global _model_cache
47
+ if _model_cache is None:
48
+ print("📦 Chargement du modèle EfficientNetV2M...")
49
+ model = tf.keras.models.load_model("model/best_efficientnetv2m_gradcam.keras", compile=False)
50
+
51
+ _model_cache = [{
52
+ "model_name": "EfficientNetV2M",
53
+ "model": model,
54
+ "gradcam_model": model,
55
+ "preprocess_input": effnet_preprocess,
56
+ "target_size": (480, 480),
57
+ "last_conv_layer": "block7a_expand_conv",
58
+ "gradcam_type": "gradcam++"
59
+ }]
60
+ return _model_cache
61
+
62
+
63
+
64
+ def compute_gradcam(model, image_array, class_index=None, layer_name=None,gradcam_type="gradcam"):
65
+ """
66
+ Calcule la carte Grad-CAM pour une image et un modèle Keras.
67
+
68
+ Args:
69
+ model: tf.keras.Model.
70
+ image_array: np.array (H, W, 3), float32, pré-traitée.
71
+ class_index: int ou None, index de la classe cible. Si None, classe prédite.
72
+ layer_name: str ou None, nom de la couche convolutionnelle à utiliser. Si None, dernière conv.
73
+
74
+ Returns:
75
+ gradcam_map: np.array (H, W), normalisée entre 0 et 1.
76
+ """
77
+ logging.info(f"Lancement calcul de la gradcam avec le type {gradcam_type}")
78
+
79
+ if image_array.ndim == 3:
80
+ input_tensor = np.expand_dims(image_array, axis=0)
81
+ else:
82
+ input_tensor = image_array
83
+ if gradcam_type=="gradcam++":
84
+ gradcam = GradcamPlusPlus(model, clone=False)
85
+ else:
86
+ gradcam = Gradcam(model, clone=False)
87
+
88
+ def loss(output):
89
+ if class_index is None:
90
+ class_index_local = tf.argmax(output[0])
91
+ else:
92
+ class_index_local = class_index
93
+ return output[:, class_index_local]
94
+
95
+ # Choisir la couche à utiliser pour GradCAM
96
+ if layer_name is None:
97
+ # Si non spécifié, chercher la dernière couche conv 2D
98
+ for layer in reversed(model.layers):
99
+ if 'conv' in layer.name and len(layer.output_shape) == 4:
100
+ layer_name = layer.name
101
+ break
102
+ if layer_name is None:
103
+ raise ValueError("Aucune couche convolutionnelle 2D trouvée dans le modèle.")
104
+
105
+ cam = gradcam(loss, input_tensor, penultimate_layer=layer_name)
106
+ cam = cam[0]
107
+
108
+ # Normaliser entre 0 et 1
109
+ cam = normalize(cam)
110
+
111
+ return cam
112
+
113
+
114
+ def preprocess_image(image_bytes, target_size, preprocess_input):
115
+ try:
116
+ logger.info("📤 Lecture des bytes et conversion en image PIL")
117
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
118
+ except Exception as e:
119
+ logger.exception("❌ Erreur lors de l'ouverture de l'image")
120
+ raise ValueError("Impossible de décoder l'image") from e
121
+
122
+ logger.info(f"📐 Redimensionnement de l'image à la taille {target_size}")
123
+ image = image.resize(target_size)
124
+ image_array = np.array(image).astype(np.float32)
125
+
126
+ logger.debug(f"🔍 Shape de l'image après conversion en tableau : {image_array.shape}")
127
+
128
+ if image_array.ndim != 3 or image_array.shape[-1] != 3:
129
+ logger.error(f"❌ Image invalide : shape={image_array.shape}")
130
+ raise ValueError("Image must have 3 channels (RGB)")
131
+
132
+ logger.info("🎨 Conversion et prétraitement de l'image")
133
+
134
+ # Préparation pour la prédiction
135
+ preprocessed_input = preprocess_input(image_array.copy())
136
+ preprocessed_input = np.expand_dims(preprocessed_input, axis=0)
137
+
138
+ # Préparation pour Grad-CAM (non prétraitée, mais batchifiée et en float32)
139
+ raw_input = np.expand_dims(image_array / 255.0, axis=0) # Mise à l’échelle simple
140
+
141
+ logger.debug(f"🧪 Shape après ajout de la dimension batch : {preprocessed_input.shape}")
142
+ return preprocessed_input, raw_input
143
+
144
+
145
+
146
+ def compute_entropy_safe(probas):
147
+ probas = np.array(probas)
148
+ # On garde uniquement les probabilités strictement positives
149
+ mask = probas > 0
150
+ entropy = -np.sum(probas[mask] * np.log(probas[mask]))
151
+ return entropy
152
+
153
+
154
+ def predict_with_model(config, image_bytes: bytes,show_heatmap=False):
155
+
156
+ input_array,raw_input = preprocess_image(image_bytes,config["target_size"],config["preprocess_input"])
157
+
158
+ logger.info("🤖 Lancement de la prédiction avec le modèle")
159
+ preds = config["model"].predict(input_array)
160
+ logger.debug(f"📈 Prédictions brutes : {preds[0].tolist()}")
161
+
162
+ predicted_class_index = int(np.argmax(preds[0]))
163
+ confidence = float(preds[0][predicted_class_index])
164
+ entropy=float(compute_entropy_safe(preds))
165
+ is_uncertain_model= (confidence<confidence_threshold) or (entropy>entropy_threshold)
166
+ logger.info(f"✅ Prédiction : classe={predicted_class_index}, confiance={confidence:.4f},entropy={entropy:.4f},is_uncertain_model={is_uncertain_model}")
167
+
168
+ result= {
169
+ "preds": preds[0].tolist(),
170
+ "predicted_class": predicted_class_index,
171
+ "confidence": confidence,
172
+ "entropy":entropy,
173
+ "is_uncertain_model":is_uncertain_model
174
+ }
175
+ if show_heatmap and not is_uncertain_model:
176
+ try:
177
+ logger.info("✅ Début de la génération de la heatmap")
178
+ start_time = time.time()
179
+
180
+ # Vérification des entrées
181
+ logger.info(f"🖼️ Image d'entrée shape: {raw_input.shape}")
182
+ logger.info(f"🎯 Index de classe prédite: {predicted_class_index}")
183
+ logger.info(f"🛠️ Dernière couche utilisée: {config['last_conv_layer']}")
184
+
185
+ # Calcul de la heatmap
186
+ heatmap = compute_gradcam(config["gradcam_model"], raw_input, class_index=predicted_class_index, layer_name=config["last_conv_layer"],gradcam_type=config["gradcam_type"])
187
+
188
+ elapsed_time = time.time() - start_time
189
+ logger.info(f"✅ Heatmap générée en {elapsed_time:.2f} secondes")
190
+
191
+ # Conversion en liste pour le JSON
192
+ result["heatmap"] = heatmap.tolist()
193
+
194
+ except Exception as e:
195
+ logger.error(f"❌ Erreur lors de la génération de la heatmap: {e}")
196
+ result["heatmap"] = []
197
+ else:
198
+ logger.info("ℹ️ Heatmap non générée (option désactivée ou modèle incertain)")
199
+ result["heatmap"] = []
200
+
201
+
202
+ return result
app/utils.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import time
3
+ from app.log import logger
4
+
5
+ from app.config import MODEL_NAME, ORCHESTRATOR_URL, OWN_URL,MODEL_TYPE
6
+
7
+ def heartbeat():
8
+ while True:
9
+ try:
10
+ response = requests.post(
11
+ f"{ORCHESTRATOR_URL}/heartbeat",
12
+ json={"model_name": MODEL_NAME},
13
+ timeout=5
14
+ )
15
+ if response.status_code == 200:
16
+ logger.info(f"💓 Heartbeat envoyé pour {MODEL_NAME}")
17
+
18
+ elif response.status_code == 404:
19
+ logger.warning(f"⚠️ Modèle inconnu dans orchestrateur (404) → tentative de réenregistrement")
20
+ # Tentative de réenregistrement à chaud
21
+ register_with_orchestrator()
22
+ else:
23
+ logger.warning(f"⚠️ Heartbeat refusé ({response.status_code}) : {response.text}")
24
+ except Exception as e:
25
+ logger.error(f"❌ Erreur lors du heartbeat : {e}")
26
+ time.sleep(60)
27
+
28
+ def register_with_orchestrator():
29
+ try:
30
+ logger.info(f"📡 Tentative d'enregistrement de {MODEL_NAME} à l'orchestrateur...")
31
+ response = requests.post(
32
+ f"{ORCHESTRATOR_URL}/register_model",
33
+ json={"model_name": MODEL_NAME, "model_type": MODEL_TYPE,"url": f"{OWN_URL}/predict"}
34
+ )
35
+ if response.status_code == 200:
36
+ logger.info("✅ Modèle enregistré avec succès")
37
+ return True
38
+ else:
39
+ logger.info(f"⚠️ Échec enregistrement : {response.text}")
40
+ return False
41
+ except Exception as e:
42
+ logger.info(f"❌ Erreur d'enregistrement : {e}")
43
+
44
+ def register_forever(interval=30):
45
+ while True:
46
+ success = register_with_orchestrator()
47
+ if success:
48
+ break # On arrête de réessayer
49
+ logger.info(f"⏳ Nouvel essai dans {interval} secondes...")
50
+ time.sleep(interval)
model/best_efficientnetv2m_gradcam.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:303b262f1a720ec476e5521f0728843c90650dabbe0adfc7c7a5a33e6fd9e9d2
3
+ size 216600984
requirements.txt ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ absl-py==1.4.0
2
+ aiohappyeyeballs==2.6.1
3
+ aiohttp==3.12.13
4
+ aiosignal==1.3.2
5
+ annotated-types==0.7.0
6
+ anyio==4.9.0
7
+ asttokens==3.0.0
8
+ astunparse==1.6.3
9
+ attrs==25.3.0
10
+ certifi==2025.6.15
11
+ charset-normalizer==3.4.2
12
+ click==8.2.1
13
+ comm==0.2.2
14
+ contourpy==1.3.2
15
+ cycler==0.12.1
16
+ debugpy==1.8.14
17
+ decorator==5.2.1
18
+ Deprecated==1.2.18
19
+ dm-tree==0.1.9
20
+ executing==2.2.0
21
+ fastapi==0.115.12
22
+ flatbuffers==25.2.10
23
+ fonttools==4.58.4
24
+ frozenlist==1.7.0
25
+ gast==0.6.0
26
+ google-pasta==0.2.0
27
+ grpcio==1.73.0
28
+ h11==0.16.0
29
+ h5py==3.14.0
30
+ idna==3.10
31
+ imageio==2.37.0
32
+ ipykernel==6.29.5
33
+ ipython==9.3.0
34
+ ipython_pygments_lexers==1.1.1
35
+ jedi==0.19.2
36
+ jupyter_client==8.6.3
37
+ jupyter_core==5.8.1
38
+ keras==3.10.0
39
+ kiwisolver==1.4.8
40
+ libclang==18.1.1
41
+ Markdown==3.8
42
+ markdown-it-py==3.0.0
43
+ MarkupSafe==3.0.2
44
+ matplotlib==3.10.3
45
+ matplotlib-inline==0.1.7
46
+ mdurl==0.1.2
47
+ ml_dtypes==0.5.1
48
+ multidict==6.4.4
49
+ namex==0.1.0
50
+ nest-asyncio==1.6.0
51
+ numpy==1.26.4
52
+ opencv-python==4.11.0.86
53
+ opt_einsum==3.4.0
54
+ optree==0.16.0
55
+ packaging==25.0
56
+ parso==0.8.4
57
+ pexpect==4.9.0
58
+ pillow==11.2.1
59
+ platformdirs==4.3.8
60
+ prompt_toolkit==3.0.51
61
+ propcache==0.3.2
62
+ protobuf==5.29.5
63
+ psutil==7.0.0
64
+ ptyprocess==0.7.0
65
+ pure_eval==0.2.3
66
+ pydantic==2.11.7
67
+ pydantic_core==2.33.2
68
+ Pygments==2.19.1
69
+ pyparsing==3.2.3
70
+ python-dateutil==2.9.0.post0
71
+ python-multipart==0.0.20
72
+ pyzmq==27.0.0
73
+ requests==2.32.4
74
+ rich==14.0.0
75
+ scipy==1.16.0
76
+ setuptools==80.9.0
77
+ six==1.17.0
78
+ sniffio==1.3.1
79
+ stack-data==0.6.3
80
+ starlette==0.46.2
81
+ tensorboard==2.19.0
82
+ tensorboard-data-server==0.7.2
83
+ tensorflow==2.19.0
84
+ tensorflow-model-optimization==0.8.0
85
+ termcolor==3.1.0
86
+ tf-keras-vis==0.8.7
87
+ tornado==6.5.1
88
+ traitlets==5.14.3
89
+ typing-inspection==0.4.1
90
+ typing_extensions==4.14.0
91
+ urllib3==2.4.0
92
+ uvicorn==0.34.3
93
+ wcwidth==0.2.13
94
+ Werkzeug==3.1.3
95
+ wheel==0.45.1
96
+ wrapt==1.17.2
97
+ yarl==1.20.1