rkonan commited on
Commit
63dce9e
·
1 Parent(s): a42c38e

passage en full distribué

Browse files
Files changed (8) hide show
  1. Dockerfile copy +0 -39
  2. README.md +2 -0
  3. app/config.py +11 -0
  4. app/log.py +15 -0
  5. app/main.py +33 -23
  6. app/main_old.py +75 -0
  7. app/model.py +1 -1
  8. app/utils.py +50 -0
Dockerfile copy DELETED
@@ -1,39 +0,0 @@
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
@@ -9,3 +9,5 @@ short_description: Reco EfficientNet API
9
  ---
10
 
11
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
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,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ ENV = os.getenv("API_ENV", "space")
4
+ MODEL_NAME = os.getenv("MODEL_NAME", "EfficientNetV2M")
5
+
6
+ if ENV == "local":
7
+ ORCHESTRATOR_URL = "http://0.0.0.0:7860"
8
+ OWN_URL = "http://0.0.0.0:7861"
9
+ else:
10
+ ORCHESTRATOR_URL = "https://rkonan-reco-orchestrator-api.hf.space"
11
+ 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 CHANGED
@@ -1,33 +1,40 @@
 
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),
@@ -42,16 +49,12 @@ async def predict(request: Request,
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")
@@ -59,7 +62,10 @@ async def predict(request: Request,
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
@@ -72,4 +78,8 @@ async def predict(request: Request,
72
 
73
  @app.get("/health")
74
  def health_check():
75
- return {"status": "ok"}
 
 
 
 
 
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
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),
 
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")
 
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
 
78
 
79
  @app.get("/health")
80
  def health_check():
81
+ return {
82
+ "status": "ok",
83
+ "model_name": MODEL_NAME,
84
+ "timestamp": time.time()
85
+ }
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 CHANGED
@@ -28,7 +28,7 @@ logging.basicConfig(
28
  format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
29
  )
30
  logger = logging.getLogger(__name__)
31
- confidence_threshold=0.5
32
  entropy_threshold=2
33
 
34
  class ModelStruct(TypedDict):
 
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):
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
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, "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)