Omarelrayes commited on
Commit
a930b94
·
verified ·
1 Parent(s): 82dbc73

Upload 5 files

Browse files
Files changed (5) hide show
  1. app/__init__.py +9 -13
  2. app/configs.py +65 -121
  3. app/main.py +179 -85
  4. app/models.py +28 -15
  5. app/storage.py +86 -18
app/__init__.py CHANGED
@@ -1,20 +1,16 @@
1
- from .configs import (
2
- get_classification_model,
3
- get_segmentation_model,
4
- model_classes,
5
- request_history,
6
- IMAGES_DIR,
7
- SEGMENTS_DIR
8
- )
9
- from .models import PredictionResponse, HistoryItem
10
 
11
  __all__ = [
12
  "get_classification_model",
13
  "get_segmentation_model",
14
- "model_classes",
15
  "request_history",
 
16
  "IMAGES_DIR",
17
  "SEGMENTS_DIR",
18
- "PredictionResponse",
19
- "HistoryItem"
20
- ]
 
 
 
 
1
+ from .configs import get_classification_model, get_segmentation_model, request_history, STORAGE_DIR, IMAGES_DIR, SEGMENTS_DIR
2
+ from .models import UploadResponse, ClassifyRequest, ClassifyResponse, SegmentRequest, SegmentResponse
 
 
 
 
 
 
 
3
 
4
  __all__ = [
5
  "get_classification_model",
6
  "get_segmentation_model",
 
7
  "request_history",
8
+ "STORAGE_DIR",
9
  "IMAGES_DIR",
10
  "SEGMENTS_DIR",
11
+ "UploadResponse",
12
+ "ClassifyRequest",
13
+ "ClassifyResponse",
14
+ "SegmentRequest",
15
+ "SegmentResponse",
16
+ ]
app/configs.py CHANGED
@@ -1,150 +1,94 @@
1
- # app/configs.py
2
  import os
3
- import uuid
4
- from pathlib import Path
5
- from typing import Dict, Any, List
6
- from datetime import datetime
7
- from huggingface_hub import hf_hub_download
8
  import zipfile
9
  import shutil
10
- import tensorflow as tf
11
-
12
- os.environ["PYTHONPATH"] = "/app:" + os.environ.get("PYTHONPATH", "")
13
-
14
- try:
15
- from hf_artifact_repo import HuggingFaceArtifactRepository
16
- print("✅ HuggingFace Artifact Repository loaded!")
17
- except ImportError as e:
18
- print(f"⚠️ Could not load HF plugin: {e}")
19
 
20
  HF_TOKEN = os.getenv("HF_TOKEN")
21
- if not HF_TOKEN:
22
- raise ValueError(" HF_TOKEN environment variable is not set!")
23
- os.environ["HF_TOKEN"] = HF_TOKEN
24
-
25
- HF_REPO = "omarelrayes/mlflow-artifacts"
26
-
27
- CLASSIFIER_ZIP_PATH = "models/classifier_savedmodel.zip"
28
- SEGMENTER_ZIP_PATH = "models/segmenter_savedmodel.zip"
29
-
30
- print("="*60)
31
- print("🔧 Configuration:")
32
- print(f" - HF Repository: {HF_REPO}")
33
- print(f" - TensorFlow version: {tf.__version__}")
34
- print("="*60)
35
 
36
  _classification_model = None
37
  _segmentation_model = None
 
38
 
39
- # Storage directories
40
- STORAGE_DIR = Path("storage")
41
- IMAGES_DIR = STORAGE_DIR / "images"
42
- SEGMENTS_DIR = STORAGE_DIR / "segments"
43
 
44
- for d in [STORAGE_DIR, IMAGES_DIR, SEGMENTS_DIR]:
45
- d.mkdir(parents=True, exist_ok=True)
46
 
47
- model_classes = {
48
- 0: "benign",
49
- 1: "malignant"
50
- }
51
 
52
- request_history: List[Dict[str, Any]] = []
 
53
 
54
- def save_image(image_bytes: bytes, filename: str) -> str:
55
- """Save uploaded image to storage"""
56
- image_path = IMAGES_DIR / filename
57
- with open(image_path, 'wb') as f:
58
- f.write(image_bytes)
59
- return str(image_path)
60
-
61
- def save_segmentation(mask_image, request_id: str) -> str:
62
- """Save segmentation mask"""
63
- segmentation_path = SEGMENTS_DIR / f"{request_id}_mask.png"
64
- mask_image.save(segmentation_path, format='PNG')
65
- return str(segmentation_path)
66
-
67
- def download_and_extract_savedmodel(zip_path_in_repo, extract_dir):
68
- print(f"📥 Downloading {zip_path_in_repo}...")
69
-
70
  zip_file = hf_hub_download(
71
- repo_id=HF_REPO,
72
  filename=zip_path_in_repo,
73
  repo_type="model",
74
- token=HF_TOKEN,
75
- cache_dir="/tmp/hf_cache"
76
  )
77
-
78
- print(f"📦 Downloaded to: {zip_file}")
79
-
80
- if os.path.exists(extract_dir):
81
  shutil.rmtree(extract_dir)
82
-
83
- os.makedirs(extract_dir, exist_ok=True)
84
- print(f"📂 Extracting to: {extract_dir}")
85
-
86
- with zipfile.ZipFile(zip_file, 'r') as zipf:
87
- zipf.extractall(extract_dir)
88
-
89
- print(f"✅ Extracted successfully!")
90
- return extract_dir
 
 
 
 
 
 
 
91
 
92
  def get_classification_model():
93
  global _classification_model
94
-
95
  if _classification_model is None:
96
- print("="*60)
97
- print("🔄 Loading Classification Model...")
98
- print("="*60)
99
-
100
- try:
101
- extract_dir = download_and_extract_savedmodel(
102
- CLASSIFIER_ZIP_PATH,
103
- "/tmp/savedmodels/classifier"
104
- )
105
-
106
- print(f"🤖 Loading SavedModel from: {extract_dir}")
107
-
108
- loaded = tf.saved_model.load(extract_dir)
109
- _classification_model = loaded.signatures['serving_default']
110
- _classification_model._loaded_obj = loaded
111
-
112
- print("✅ Classification Model Loaded Successfully!")
113
- except Exception as e:
114
- print(f"❌ Error loading classification model: {e}")
115
- import traceback
116
- traceback.print_exc()
117
- raise
118
-
119
  return _classification_model
120
 
 
121
  def get_segmentation_model():
122
  global _segmentation_model
123
-
124
  if _segmentation_model is None:
125
- print("="*60)
126
- print("🔄 Loading Segmentation Model...")
127
- print("="*60)
128
-
129
- try:
130
- extract_dir = download_and_extract_savedmodel(
131
- SEGMENTER_ZIP_PATH,
132
- "/tmp/savedmodels/segmenter"
133
- )
134
-
135
- print(f"🤖 Loading SavedModel from: {extract_dir}")
136
-
137
- loaded = tf.saved_model.load(extract_dir)
138
- _segmentation_model = loaded.signatures['serving_default']
139
- _segmentation_model._loaded_obj = loaded
140
-
141
- print("✅ Segmentation Model Loaded Successfully!")
142
- except Exception as e:
143
- print(f"❌ Error loading segmentation model: {e}")
144
- import traceback
145
- traceback.print_exc()
146
- raise
147
-
148
  return _segmentation_model
149
 
150
- print("✅ Models module initialized successfully!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import os
 
 
 
 
 
2
  import zipfile
3
  import shutil
4
+ import threading
5
+ import tempfile
6
+ from pathlib import Path
7
+ from typing import Dict, Any, List, Optional
8
+ from huggingface_hub import hf_hub_download
 
 
 
 
9
 
10
  HF_TOKEN = os.getenv("HF_TOKEN")
11
+ HF_MODEL_REPO = os.getenv("HF_MODEL_REPO", "omarelrayes/mlflow-artifacts")
12
+ CLASSIFIER_MODEL_PATH = os.getenv("CLASSIFIER_MODEL_PATH", "models/classifier_savedmodel.zip")
13
+ SEGMENTER_MODEL_PATH = os.getenv("SEGMENTER_MODEL_PATH", "models/segmenter_savedmodel.zip")
14
+ MODEL_CACHE_DIR = Path(os.getenv("MODEL_CACHE_DIR", tempfile.gettempdir())) / "savedmodels"
 
 
 
 
 
 
 
 
 
 
15
 
16
  _classification_model = None
17
  _segmentation_model = None
18
+ _model_lock = threading.Lock()
19
 
20
+ CLASSIFIER_THRESHOLD = float(os.getenv("CLASSIFIER_THRESHOLD", "0.5"))
 
 
 
21
 
22
+ request_history: List[Dict[str, Any]] = []
 
23
 
 
 
 
 
24
 
25
+ def sigmoid_to_class(confidence: float) -> str:
26
+ return "malicious" if confidence >= CLASSIFIER_THRESHOLD else "benign"
27
 
28
+
29
+ def _download_and_extract(zip_path_in_repo: str, extract_subdir: str):
30
+ extract_dir = MODEL_CACHE_DIR / extract_subdir
31
+ if extract_dir.exists():
32
+ return str(extract_dir)
33
+
34
+ print(f"Downloading {zip_path_in_repo} from {HF_MODEL_REPO}...")
 
 
 
 
 
 
 
 
 
35
  zip_file = hf_hub_download(
36
+ repo_id=HF_MODEL_REPO,
37
  filename=zip_path_in_repo,
38
  repo_type="model",
39
+ token=HF_TOKEN or None,
 
40
  )
41
+
42
+ if extract_dir.exists():
 
 
43
  shutil.rmtree(extract_dir)
44
+ extract_dir.mkdir(parents=True, exist_ok=True)
45
+
46
+ with zipfile.ZipFile(zip_file, "r") as zf:
47
+ zf.extractall(extract_dir)
48
+
49
+ print(f"Extracted model to {extract_dir}")
50
+ return str(extract_dir)
51
+
52
+
53
+ def _load_tf_model(zip_path: str, subdir: str):
54
+ import tensorflow as tf
55
+
56
+ model_dir = _download_and_extract(zip_path, subdir)
57
+ loaded = tf.saved_model.load(model_dir)
58
+ return loaded.signatures["serving_default"]
59
+
60
 
61
  def get_classification_model():
62
  global _classification_model
 
63
  if _classification_model is None:
64
+ with _model_lock:
65
+ if _classification_model is None:
66
+ print("Loading Classification Model...")
67
+ _classification_model = _load_tf_model(CLASSIFIER_MODEL_PATH, "classifier")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  return _classification_model
69
 
70
+
71
  def get_segmentation_model():
72
  global _segmentation_model
 
73
  if _segmentation_model is None:
74
+ with _model_lock:
75
+ if _segmentation_model is None:
76
+ print("Loading Segmentation Model...")
77
+ _segmentation_model = _load_tf_model(SEGMENTER_MODEL_PATH, "segmenter")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  return _segmentation_model
79
 
80
+
81
+ def get_loaded_versions() -> List[str]:
82
+ return ["hf_savedmodel"]
83
+
84
+
85
+ CLASSIFIER_URI = os.getenv("CLASSIFIER_MODEL_URI", "hf_savedmodel")
86
+ SEGMENTER_URI = os.getenv("SEGMENTER_MODEL_URI", "hf_savedmodel")
87
+
88
+ STORAGE_DIR = Path(os.getenv("STORAGE_DIR", "storage"))
89
+ IMAGES_DIR = STORAGE_DIR / "images"
90
+ SEGMENTS_DIR = STORAGE_DIR / "segments"
91
+ RESULTS_DIR = STORAGE_DIR / "results"
92
+
93
+ for dir_path in [STORAGE_DIR, IMAGES_DIR, SEGMENTS_DIR, RESULTS_DIR]:
94
+ dir_path.mkdir(parents=True, exist_ok=True)
app/main.py CHANGED
@@ -1,22 +1,41 @@
1
  import uuid
2
  from pathlib import Path
3
  from datetime import datetime
4
- from fastapi import FastAPI, UploadFile, File, BackgroundTasks, HTTPException
5
- from typing import List
6
-
7
- from app import configs as config
8
- from app.models import PredictionResponse, HistoryItem
9
- from app.services.predictor import predict_image
10
- from app.services.segmenter import run_segmentation
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  app = FastAPI(
13
  title="AI Image Classification API",
14
- version="2.0.0"
15
  )
16
 
17
- # =========================
18
- # Models Status
19
- # =========================
 
 
 
 
 
 
 
 
20
  @app.get("/models/status")
21
  async def models_status():
22
  classification_model = config.get_classification_model()
@@ -25,115 +44,190 @@ async def models_status():
25
  return {
26
  "classification_loaded": classification_model is not None,
27
  "segmentation_loaded": segmentation_model is not None,
 
 
28
  "storage_paths": {
29
  "images": str(config.IMAGES_DIR),
30
- "segments": str(config.SEGMENTS_DIR)
31
- }
32
  }
33
 
34
- # =========================
35
- # Prediction Endpoint
36
- # =========================
37
- @app.post("/predict", response_model=PredictionResponse)
38
- async def predict(
39
- background_tasks: BackgroundTasks,
40
- file: UploadFile = File(...)
41
- ):
 
 
 
 
 
 
42
  file_bytes = await file.read()
43
 
44
- # Validation
45
- if len(file_bytes) > 10 * 1024 * 1024:
46
  raise HTTPException(status_code=400, detail="File too large (10MB max)")
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  try:
49
- prediction, confidence, image_path = predict_image(
50
- file_bytes,
51
- file.filename or "image.jpg"
52
  )
 
 
53
  except Exception as e:
54
  raise HTTPException(status_code=500, detail=str(e))
55
 
56
- # Get model version
57
- model_version = "savedmodel-v1"
58
-
59
- # Create history
60
- request_id = str(uuid.uuid4())
61
-
62
- history_item = {
63
- "request_id": request_id,
64
- "filename": file.filename or "image.jpg",
65
- "image_path": image_path,
66
  "prediction": prediction,
67
  "confidence": confidence,
68
- "model_version": model_version,
69
- "timestamp": datetime.now().isoformat(),
70
- "status": "classified",
71
- "segmentation_path": None
72
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- config.request_history.append(history_item)
 
75
 
76
- # 🔥 Run segmentation in background if malignant
77
- if prediction.lower() == "malignant":
78
- background_tasks.add_task(
79
- run_segmentation,
80
- request_id,
81
- Path(image_path).name,
82
- image_path
83
- )
84
 
85
- return PredictionResponse(**history_item)
86
 
87
- # =========================
88
- # History
89
- # =========================
90
- @app.get("/history", response_model=List[HistoryItem])
91
- async def get_history():
92
- return [HistoryItem(**item) for item in config.request_history]
93
 
94
- @app.get("/history/{request_id}", response_model=HistoryItem)
95
- async def get_prediction(request_id: str):
96
- for item in config.request_history:
97
- if item["request_id"] == request_id:
98
- return HistoryItem(**item)
 
 
 
99
 
100
- raise HTTPException(status_code=404, detail="Prediction not found")
101
 
102
- # =========================
103
- # Root
104
- # =========================
105
  @app.get("/")
106
  async def root():
107
- class_ready = "READY" if config.get_classification_model() else "REQUIRED !!!!"
108
- seg_ready = "READY" if config.get_segmentation_model() else "OPTIONAL !"
109
 
110
  return {
111
  "service": "AI Image Classification API",
 
112
  "classification": class_ready,
113
  "segmentation": seg_ready,
114
- "endpoint": "POST /predict",
115
- "history": f"GET /history ({len(config.request_history)} records)",
116
- "docs": "/docs"
 
 
 
 
 
 
 
117
  }
118
 
119
- # =========================
120
- # Health Check
121
- # =========================
122
  @app.get("/health")
123
  async def health():
124
  return {
125
  "status": "healthy",
126
  "predict_ready": config.get_classification_model() is not None,
127
- "total_predictions": len(config.request_history)
128
  }
129
-
130
- # =========================
131
- # Run Server
132
- # =========================
133
- if __name__ == "__main__":
134
- import uvicorn
135
- uvicorn.run(
136
- "app.main:app",
137
- host="0.0.0.0",
138
- port=7860
139
- )
 
1
  import uuid
2
  from pathlib import Path
3
  from datetime import datetime
4
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Request
5
+ from fastapi.responses import FileResponse
6
+
7
+ from . import configs as config
8
+ from .models import UploadResponse, ClassifyRequest, ClassifyResponse, SegmentRequest, SegmentResponse
9
+ from .services.predictor import classify_image
10
+ from .services.segmenter import segment_image
11
+ from .services.routing import router as model_router
12
+ from .storage import (
13
+ save_image,
14
+ get_image_path,
15
+ get_image_bytes,
16
+ save_classification_result,
17
+ get_classification_result,
18
+ save_segmentation_result,
19
+ get_segmentation_result,
20
+ )
21
+ from .monitoring.metrics import MetricsMiddleware, metrics_endpoint
22
 
23
  app = FastAPI(
24
  title="AI Image Classification API",
25
+ version="3.1.0"
26
  )
27
 
28
+ app.add_middleware(MetricsMiddleware)
29
+
30
+ ALLOWED_CONTENT_TYPES = {"image/jpeg", "image/png", "image/jpg"}
31
+ MAX_FILE_SIZE = 10 * 1024 * 1024
32
+
33
+
34
+ @app.get("/metrics")
35
+ async def prometheus_metrics(request: Request):
36
+ return metrics_endpoint(request)
37
+
38
+
39
  @app.get("/models/status")
40
  async def models_status():
41
  classification_model = config.get_classification_model()
 
44
  return {
45
  "classification_loaded": classification_model is not None,
46
  "segmentation_loaded": segmentation_model is not None,
47
+ "ab_testing": model_router.get_ab_status(),
48
+ "loaded_versions": config.get_loaded_versions(),
49
  "storage_paths": {
50
  "images": str(config.IMAGES_DIR),
51
+ "segments": str(config.SEGMENTS_DIR),
52
+ },
53
  }
54
 
55
+
56
+ @app.get("/api/ab-status")
57
+ async def ab_testing_status():
58
+ return model_router.get_ab_status()
59
+
60
+
61
+ @app.post("/api/upload", status_code=201, response_model=UploadResponse)
62
+ async def upload_image(file: UploadFile = File(...)):
63
+ if file.content_type not in ALLOWED_CONTENT_TYPES:
64
+ raise HTTPException(
65
+ status_code=400,
66
+ detail=f"Unsupported content type: {file.content_type}. Allowed: {ALLOWED_CONTENT_TYPES}",
67
+ )
68
+
69
  file_bytes = await file.read()
70
 
71
+ if len(file_bytes) > MAX_FILE_SIZE:
 
72
  raise HTTPException(status_code=400, detail="File too large (10MB max)")
73
 
74
+ ext = Path(file.filename or "image.jpg").suffix or ".jpg"
75
+ image_id = str(uuid.uuid4())
76
+
77
+ save_image(image_id, file_bytes, ext)
78
+
79
+ return UploadResponse(
80
+ image_id=image_id,
81
+ filename=file.filename or "image.jpg",
82
+ size_bytes=len(file_bytes),
83
+ content_type=file.content_type,
84
+ uploaded_at=datetime.now().isoformat(),
85
+ url=f"/api/images/{image_id}",
86
+ )
87
+
88
+
89
+ @app.get("/api/images/{image_id}")
90
+ async def get_image(image_id: str):
91
+ image_path = get_image_path(image_id)
92
+ if image_path is None:
93
+ raise HTTPException(status_code=404, detail="Image not found")
94
+
95
+ media_type = "image/jpeg"
96
+ if image_path.suffix.lower() in {".png"}:
97
+ media_type = "image/png"
98
+
99
+ return FileResponse(path=image_path, media_type=media_type)
100
+
101
+
102
+ @app.post("/api/classify", response_model=ClassifyResponse)
103
+ async def classify(body: ClassifyRequest):
104
+ image_bytes = get_image_bytes(body.image_id)
105
+ if image_bytes is None:
106
+ raise HTTPException(status_code=404, detail="Image not found")
107
+
108
+ existing = get_classification_result(body.image_id)
109
+ if existing and not body.model_version:
110
+ return ClassifyResponse(
111
+ image_id=body.image_id,
112
+ prediction=existing["prediction"],
113
+ confidence=existing["confidence"],
114
+ model_version=existing["model_version"],
115
+ status=existing.get("status", "completed"),
116
+ )
117
+
118
+ resolved_uri = model_router.resolve_classifier_version(body.model_version)
119
+
120
  try:
121
+ prediction, confidence, actual_version = classify_image(
122
+ image_bytes,
123
+ model_version=resolved_uri,
124
  )
125
+ except ValueError as e:
126
+ raise HTTPException(status_code=400, detail=str(e))
127
  except Exception as e:
128
  raise HTTPException(status_code=500, detail=str(e))
129
 
130
+ result = {
 
 
 
 
 
 
 
 
 
131
  "prediction": prediction,
132
  "confidence": confidence,
133
+ "model_version": actual_version,
134
+ "status": "completed",
 
 
135
  }
136
+ save_classification_result(body.image_id, result)
137
+
138
+ return ClassifyResponse(
139
+ image_id=body.image_id,
140
+ **result,
141
+ )
142
+
143
+
144
+ @app.get("/api/classify/{image_id}", response_model=ClassifyResponse)
145
+ async def get_classify_result(image_id: str):
146
+ result = get_classification_result(image_id)
147
+ if result is None:
148
+ raise HTTPException(status_code=404, detail="Classification result not found")
149
+
150
+ return ClassifyResponse(
151
+ image_id=image_id,
152
+ prediction=result["prediction"],
153
+ confidence=result["confidence"],
154
+ model_version=result["model_version"],
155
+ status=result.get("status", "completed"),
156
+ )
157
+
158
+
159
+ @app.post("/api/segment", response_model=SegmentResponse)
160
+ async def segment(body: SegmentRequest):
161
+ image_bytes = get_image_bytes(body.image_id)
162
+ if image_bytes is None:
163
+ raise HTTPException(status_code=404, detail="Image not found")
164
+
165
+ existing = get_segmentation_result(body.image_id)
166
+ if existing:
167
+ return SegmentResponse(
168
+ image_id=body.image_id,
169
+ status=existing.get("status", "completed"),
170
+ masks_shape=existing.get("masks_shape"),
171
+ max_confidence=existing.get("max_confidence"),
172
+ result_url=existing.get("result_path"),
173
+ error=existing.get("error"),
174
+ )
175
 
176
+ seg_result = segment_image(image_bytes)
177
+ seg_path = save_segmentation_result(body.image_id, seg_result)
178
 
179
+ return SegmentResponse(
180
+ image_id=body.image_id,
181
+ status=seg_result.get("status", "completed"),
182
+ masks_shape=seg_result.get("masks_shape"),
183
+ max_confidence=seg_result.get("max_confidence"),
184
+ result_url=seg_path,
185
+ error=seg_result.get("error"),
186
+ )
187
 
 
188
 
189
+ @app.get("/api/segment/{image_id}", response_model=SegmentResponse)
190
+ async def get_segment_result(image_id: str):
191
+ result = get_segmentation_result(image_id)
192
+ if result is None:
193
+ raise HTTPException(status_code=404, detail="Segmentation result not found")
 
194
 
195
+ return SegmentResponse(
196
+ image_id=image_id,
197
+ status=result.get("status", "completed"),
198
+ masks_shape=result.get("masks_shape"),
199
+ max_confidence=result.get("max_confidence"),
200
+ result_url=result.get("result_path"),
201
+ error=result.get("error"),
202
+ )
203
 
 
204
 
 
 
 
205
  @app.get("/")
206
  async def root():
207
+ class_ready = "READY" if config.get_classification_model() else "REQUIRED"
208
+ seg_ready = "READY" if config.get_segmentation_model() else "OPTIONAL"
209
 
210
  return {
211
  "service": "AI Image Classification API",
212
+ "version": "3.1.0",
213
  "classification": class_ready,
214
  "segmentation": seg_ready,
215
+ "endpoints": {
216
+ "upload": "POST /api/upload",
217
+ "classify": "POST /api/classify",
218
+ "segment": "POST /api/segment",
219
+ "health": "GET /health",
220
+ "metrics": "GET /metrics",
221
+ "ab_status": "GET /api/ab-status",
222
+ "docs": "/docs",
223
+ },
224
+ "models_status": "/models/status",
225
  }
226
 
227
+
 
 
228
  @app.get("/health")
229
  async def health():
230
  return {
231
  "status": "healthy",
232
  "predict_ready": config.get_classification_model() is not None,
 
233
  }
 
 
 
 
 
 
 
 
 
 
 
app/models.py CHANGED
@@ -1,24 +1,37 @@
1
  from pydantic import BaseModel
2
  from typing import Optional
3
 
4
- class PredictionResponse(BaseModel):
5
- request_id: str
6
- filename: str
7
- image_path: str
8
- prediction: str
9
- confidence: float
10
- model_version: str
11
- timestamp: str
12
- status: str
13
- segmentation_path: Optional[str] = None
14
 
15
- class HistoryItem(BaseModel):
16
- request_id: str
17
  filename: str
18
- image_path: str
 
 
 
 
 
 
 
 
 
 
 
 
19
  prediction: str
20
  confidence: float
21
  model_version: str
22
- timestamp: str
 
 
 
 
 
 
 
 
23
  status: str
24
- segmentation_path: Optional[str] = None
 
 
 
 
1
  from pydantic import BaseModel
2
  from typing import Optional
3
 
 
 
 
 
 
 
 
 
 
 
4
 
5
+ class UploadResponse(BaseModel):
6
+ image_id: str
7
  filename: str
8
+ size_bytes: int
9
+ content_type: str
10
+ uploaded_at: str
11
+ url: str
12
+
13
+
14
+ class ClassifyRequest(BaseModel):
15
+ image_id: str
16
+ model_version: Optional[str] = None
17
+
18
+
19
+ class ClassifyResponse(BaseModel):
20
+ image_id: str
21
  prediction: str
22
  confidence: float
23
  model_version: str
24
+ status: str = "completed"
25
+
26
+
27
+ class SegmentRequest(BaseModel):
28
+ image_id: str
29
+
30
+
31
+ class SegmentResponse(BaseModel):
32
+ image_id: str
33
  status: str
34
+ masks_shape: Optional[list] = None
35
+ max_confidence: Optional[float] = None
36
+ result_url: Optional[str] = None
37
+ error: Optional[str] = None
app/storage.py CHANGED
@@ -1,25 +1,93 @@
1
- from pathlib import Path
2
  import json
3
- from typing import List, Dict, Any
4
- from app.configs import IMAGES_DIR, SEGMENTS_DIR, request_history
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- def save_image(image_bytes: bytes, filename: str) -> str:
7
- """Save image to storage"""
8
- image_path = IMAGES_DIR / filename
 
 
 
 
9
  with open(image_path, "wb") as f:
10
- f.write(image_bytes)
11
- return str(image_path)
 
 
 
 
 
 
 
 
 
12
 
13
- def save_segmentation_mask(mask_image, request_id: str) -> str:
14
- """Save segmentation mask as PNG"""
15
- seg_filename = f"{request_id}_mask.png"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  seg_path = SEGMENTS_DIR / seg_filename
17
- mask_image.save(seg_path, format='PNG')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  return str(seg_path)
19
 
20
- def update_history(request_id: str, **updates: Any) -> None:
21
- """Update history item by request_id"""
22
- for item in request_history:
23
- if item["request_id"] == request_id:
24
- item.update(updates)
25
- break
 
 
 
 
 
 
 
 
1
  import json
2
+ import threading
3
+ from pathlib import Path
4
+ from typing import Optional
5
+ from app.configs import IMAGES_DIR, SEGMENTS_DIR, RESULTS_DIR
6
+
7
+ _results: dict = {}
8
+ _results_lock = threading.Lock()
9
+ _RESULTS_FILE = RESULTS_DIR / "results.json"
10
+
11
+
12
+ def _load_results():
13
+ global _results
14
+ if _RESULTS_FILE.exists():
15
+ with open(_RESULTS_FILE, "r") as f:
16
+ _results = json.load(f)
17
+
18
 
19
+ def _persist_results():
20
+ with open(_RESULTS_FILE, "w") as f:
21
+ json.dump(_results, f, indent=2)
22
+
23
+
24
+ def save_image(image_id: str, file_bytes: bytes, extension: str = ".jpg") -> Path:
25
+ image_path = IMAGES_DIR / f"{image_id}{extension}"
26
  with open(image_path, "wb") as f:
27
+ f.write(file_bytes)
28
+ return image_path
29
+
30
+
31
+ def get_image_path(image_id: str) -> Optional[Path]:
32
+ for ext in [".jpg", ".jpeg", ".png", ".JPG", ".JPEG", ".PNG"]:
33
+ path = IMAGES_DIR / f"{image_id}{ext}"
34
+ if path.exists():
35
+ return path
36
+ return None
37
+
38
 
39
+ def get_image_bytes(image_id: str) -> Optional[bytes]:
40
+ path = get_image_path(image_id)
41
+ if path is None:
42
+ return None
43
+ with open(path, "rb") as f:
44
+ return f.read()
45
+
46
+
47
+ def save_classification_result(image_id: str, result: dict) -> None:
48
+ with _results_lock:
49
+ if image_id not in _results:
50
+ _results[image_id] = {}
51
+ _results[image_id]["classification"] = result
52
+ _persist_results()
53
+
54
+
55
+ def get_classification_result(image_id: str) -> Optional[dict]:
56
+ with _results_lock:
57
+ if not _results:
58
+ _load_results()
59
+ return _results.get(image_id, {}).get("classification")
60
+
61
+
62
+ def save_segmentation_result(image_id: str, seg_result: dict) -> str:
63
+ seg_filename = f"{image_id}_segment.json"
64
  seg_path = SEGMENTS_DIR / seg_filename
65
+ with open(seg_path, "w") as f:
66
+ json.dump(seg_result, f, indent=2)
67
+
68
+ with _results_lock:
69
+ if image_id not in _results:
70
+ _results[image_id] = {}
71
+ _results[image_id]["segmentation"] = {
72
+ "result_path": str(seg_path),
73
+ "status": seg_result.get("status", "completed"),
74
+ "masks_shape": seg_result.get("masks_shape"),
75
+ "max_confidence": seg_result.get("max_confidence"),
76
+ "error": seg_result.get("error"),
77
+ }
78
+ _persist_results()
79
+
80
  return str(seg_path)
81
 
82
+
83
+ def get_segmentation_result(image_id: str) -> Optional[dict]:
84
+ with _results_lock:
85
+ if not _results:
86
+ _load_results()
87
+ return _results.get(image_id, {}).get("segmentation")
88
+
89
+
90
+ def get_segmentation_result_path(image_id: str) -> Optional[str]:
91
+ for f in SEGMENTS_DIR.glob(f"{image_id}_segment.json"):
92
+ return str(f)
93
+ return None