sairamv1 commited on
Commit
3c33ae2
Β·
verified Β·
1 Parent(s): deb0b65

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +312 -221
app.py CHANGED
@@ -1,264 +1,355 @@
1
  import os
 
 
 
 
 
 
 
2
  import base64
3
  import logging
4
- import traceback
5
- from flask import Flask, request, jsonify, render_template
6
- from flask_cors import CORS
7
- from dotenv import load_dotenv
8
- from roboflow import Roboflow
9
-
10
- # ─── Configuration ───────────────────────────────────────────────────────
11
- load_dotenv()
12
 
13
- app = Flask(__name__)
14
- CORS(app)
 
 
 
 
15
 
16
- # Configure logging
17
- logging.basicConfig(
18
- level=logging.INFO,
19
- format='%(asctime)s - %(levelname)s - %(message)s'
20
- )
21
  logger = logging.getLogger(__name__)
22
 
23
- # ─── Roboflow Setup ──────────────────────────────────────────────────────
24
- ROBOFLOW_API_KEY = os.getenv("ROBOFLOW_API_KEY")
25
- ROBOFLOW_WORKSPACE = os.getenv("ROBOFLOW_WORKSPACE")
26
- ROBOFLOW_PROJECT = os.getenv("ROBOFLOW_PROJECT")
27
- ROBOFLOW_VERSION = int(os.getenv("ROBOFLOW_VERSION", 1))
28
-
29
- if not all([ROBOFLOW_API_KEY, ROBOFLOW_WORKSPACE, ROBOFLOW_PROJECT]):
30
- raise ValueError("Missing required Roboflow environment variables")
31
 
32
- logger.info(f"πŸš€ Initializing Roboflow: {ROBOFLOW_WORKSPACE}/{ROBOFLOW_PROJECT}/{ROBOFLOW_VERSION}")
 
33
 
34
- try:
35
- rf = Roboflow(api_key=ROBOFLOW_API_KEY)
36
- project = rf.workspace(ROBOFLOW_WORKSPACE).project(ROBOFLOW_PROJECT)
37
- model = project.version(ROBOFLOW_VERSION).model
38
- logger.info("βœ… Roboflow model loaded successfully")
39
- except Exception as e:
40
- logger.error(f"❌ Failed to initialize Roboflow: {e}")
41
- raise
 
 
 
 
 
 
 
 
42
 
43
- # ─── Helper Functions ────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
- def decode_base64_image(base64_string):
46
- """Decode base64 image string to bytes."""
 
 
 
47
  try:
48
- # Remove data URL prefix if present
49
- if ',' in base64_string:
50
- base64_string = base64_string.split(',')[1]
 
51
 
52
- image_data = base64.b64decode(base64_string)
53
- logger.info(f"βœ… Decoded image: {len(image_data)} bytes")
54
- return image_data
55
  except Exception as e:
56
- logger.error(f"❌ Base64 decode error: {e}")
57
- raise ValueError(f"Invalid base64 image data: {str(e)}")
58
-
59
- def save_temp_image(image_data, filename="temp_image.jpg"):
60
- """Save image data to temporary file."""
61
- try:
62
- temp_path = os.path.join("/tmp", filename)
63
- with open(temp_path, "wb") as f:
64
- f.write(image_data)
65
- logger.info(f"βœ… Saved temp image: {temp_path}")
66
- return temp_path
67
- except Exception as e:
68
- logger.error(f"❌ Failed to save temp image: {e}")
69
- raise
70
-
71
- def cleanup_temp_file(filepath):
72
- """Remove temporary file."""
73
- try:
74
- if os.path.exists(filepath):
75
- os.remove(filepath)
76
- logger.info(f"πŸ—‘οΈ Cleaned up: {filepath}")
77
- except Exception as e:
78
- logger.warning(f"⚠️ Failed to cleanup {filepath}: {e}")
79
 
80
- # ─── API Routes ──────────────────────────────────────────────────────────
 
 
 
 
 
81
 
82
- @app.route('/')
83
- def index():
84
- """Serve the main HTML page."""
85
- return render_template('index.html')
 
 
 
 
 
 
 
 
 
86
 
87
- @app.route('/health', methods=['GET'])
88
- def health_check():
 
89
  """Health check endpoint."""
90
- return jsonify({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  "status": "healthy",
92
- "model": f"{ROBOFLOW_WORKSPACE}/{ROBOFLOW_PROJECT}/{ROBOFLOW_VERSION}"
93
- })
 
94
 
95
- @app.route('/detect', methods=['POST'])
96
- def detect_teeth():
97
  """
98
- Main detection endpoint.
99
- Expects JSON with 'image' field containing base64-encoded image.
 
100
  """
101
- temp_path = None
 
102
 
 
 
 
 
103
  try:
104
- # ─── Validate request ────────────────────────────────────────────
105
- data = request.get_json()
 
106
 
107
- if not data or 'image' not in data:
108
- logger.error("❌ No image data in request")
109
- return jsonify({
110
- "status": "error",
111
- "message": "No image data provided"
112
- }), 400
113
 
114
- logger.info("πŸ“₯ Received detection request")
 
115
 
116
- # ─── Decode and save image ───────────────────────────────────────
117
- image_data = decode_base64_image(data['image'])
118
- temp_path = save_temp_image(image_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- # ─── Run inference ───────────────────────────────────────────────
121
- logger.info("πŸ” Running Roboflow inference...")
122
- result = model.predict(temp_path, confidence=40, overlap=30)
123
 
124
- # Get JSON response
125
- result_json = result.json()
126
  logger.info(f"πŸ” Response Type: {type(result_json)}")
127
  logger.info(f"πŸ” Response Keys: {result_json.keys() if isinstance(result_json, dict) else 'Not a dict'}")
 
128
 
129
- # ─── Parse detections ────────────────────────────────────────────
130
- predictions = []
131
-
132
- try:
133
- # Handle different response structures
134
- if isinstance(result_json, list) and len(result_json) > 0:
135
- # Response is a list
136
- first_item = result_json[0]
137
- logger.info(f"πŸ” First item type: {type(first_item)}")
138
- logger.info(f"πŸ” First item keys: {first_item.keys() if isinstance(first_item, dict) else 'Not dict'}")
139
-
140
- # FIXED: Check for "predictions" directly first!
141
- if isinstance(first_item, dict):
142
- if "predictions" in first_item:
143
- predictions = first_item["predictions"]
144
- logger.info(f"βœ… Found predictions directly in first_item")
145
- elif "outputs" in first_item:
146
- outputs = first_item["outputs"]
147
- logger.info(f"πŸ” Found outputs: {type(outputs)}")
148
- if isinstance(outputs, dict):
149
- predictions = outputs.get("predictions", [])
150
-
151
- elif isinstance(result_json, dict):
152
- # Response is a dict
153
- logger.info(f"πŸ” Dict keys: {list(result_json.keys())}")
154
-
155
- # Try "predictions" first
156
- if "predictions" in result_json:
157
- predictions = result_json["predictions"]
158
- elif "outputs" in result_json:
159
- outputs = result_json["outputs"]
160
- if isinstance(outputs, list) and len(outputs) > 0:
161
- output_data = outputs[0]
162
- elif isinstance(outputs, dict):
163
- output_data = outputs
164
- else:
165
- output_data = {}
166
- predictions = output_data.get("predictions", [])
167
 
168
- # Handle case where predictions might be a dict
169
- if isinstance(predictions, dict) and "predictions" in predictions:
170
- predictions = predictions["predictions"]
 
 
 
 
 
 
 
 
 
 
 
171
 
172
- logger.info(f"πŸ” Final predictions type: {type(predictions)}")
173
- logger.info(f"πŸ” Final predictions count: {len(predictions) if isinstance(predictions, list) else 0}")
174
- if isinstance(predictions, list) and len(predictions) > 0:
175
- logger.info(f"πŸ” First prediction keys: {predictions[0].keys()}")
176
-
177
- except Exception as e:
178
- logger.error(f"❌ Parsing error: {e}", exc_info=True)
179
- predictions = []
180
-
181
- if not predictions or (isinstance(predictions, list) and len(predictions) == 0):
182
- logger.warning("⚠️ No predictions found after parsing")
183
- return jsonify({
184
- "status": "success",
185
- "tooth_count": 0,
186
- "results": [],
187
- "message": "No teeth detected in image"
188
- })
189
-
190
- logger.info(f"βœ… Found {len(predictions)} teeth detections")
191
 
192
- # ─── Process detections ──────────────────────────────────────────
193
- teeth_data = []
 
194
 
195
- for idx, pred in enumerate(predictions):
196
- try:
197
- detection = {
198
- "tooth_id": idx + 1,
199
- "class": pred.get("class", "tooth"),
200
- "confidence": round(pred.get("confidence", 0) * 100, 2),
201
- "bounding_box": {
202
- "x": pred.get("x", 0),
203
- "y": pred.get("y", 0),
204
- "width": pred.get("width", 0),
205
- "height": pred.get("height", 0)
206
- }
207
- }
208
-
209
- # Add detection ID if available
210
- if "detection_id" in pred:
211
- detection["detection_id"] = pred["detection_id"]
212
-
213
- teeth_data.append(detection)
214
- logger.info(f"🦷 Tooth {idx + 1}: {detection['class']} ({detection['confidence']}%)")
215
-
216
- except Exception as e:
217
- logger.error(f"❌ Error processing detection {idx}: {e}")
 
 
 
 
 
 
 
 
 
 
 
218
  continue
219
-
220
- # ─── Prepare response ────────────────────────────────────────────
221
- response = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  "status": "success",
223
- "tooth_count": len(teeth_data),
224
- "results": teeth_data,
225
- "model_info": {
226
- "workspace": ROBOFLOW_WORKSPACE,
227
- "project": ROBOFLOW_PROJECT,
228
- "version": ROBOFLOW_VERSION
229
- }
230
  }
231
-
232
- logger.info(f"βœ… Detection complete: {len(teeth_data)} teeth found")
233
- return jsonify(response)
234
-
235
- except ValueError as ve:
236
- logger.error(f"❌ Validation error: {ve}")
237
- return jsonify({
238
- "status": "error",
239
- "message": str(ve)
240
- }), 400
241
-
242
  except Exception as e:
243
- logger.error(f"❌ Unexpected error: {e}")
244
- logger.error(traceback.format_exc())
245
- return jsonify({
246
- "status": "error",
247
- "message": f"Internal server error: {str(e)}"
248
- }), 500
249
-
250
- finally:
251
- # Cleanup temp file
252
- if temp_path:
253
- cleanup_temp_file(temp_path)
254
 
255
- # ─── Main ────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
- if __name__ == '__main__':
258
- port = int(os.getenv('PORT', 5000))
259
- logger.info(f"πŸš€ Starting server on port {port}")
260
- app.run(
261
- host='0.0.0.0',
262
- port=port,
263
- debug=os.getenv('FLASK_DEBUG', 'False').lower() == 'true'
264
  )
 
1
  import os
2
+ from contextlib import asynccontextmanager
3
+
4
+ # ─── 1. MANDATORY BACKEND CONFIG (Must be at the very top) ───────────────────
5
+ os.environ["KERAS_BACKEND"] = "jax"
6
+ os.environ["JAX_PLATFORM_NAME"] = "cpu"
7
+ os.environ["HF_HOME"] = "/tmp/huggingface"
8
+
9
  import base64
10
  import logging
11
+ import numpy as np
12
+ import cv2
13
+ import requests
14
+ import tempfile
15
+ from typing import List, Dict, Any, Optional
 
 
 
16
 
17
+ import keras
18
+ from fastapi import FastAPI, File, UploadFile, HTTPException
19
+ from fastapi.responses import JSONResponse
20
+ from huggingface_hub import hf_hub_download
21
+ from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
22
+ from inference_sdk import InferenceHTTPClient
23
 
24
+ # ─── 2. LOGGING & CONFIG ─────────────────────────────────────────────────────
25
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
 
 
26
  logger = logging.getLogger(__name__)
27
 
28
+ API_KEY = os.getenv("ROBOFLOW_API_KEY")
29
+ WORKSPACE_NAME = "my-workspace-iy9ll"
30
+ WORKFLOW_ID = "find-teeth"
31
+ IMG_SIZE = 256
32
+ THRESHOLD = 0.05
33
+ MODEL_REPO = "sairamv1/TeethCariesDetection_CNNAutoencoder"
34
+ MODEL_FILENAME = "autoencoder.keras"
 
35
 
36
+ MODEL = None
37
+ roboflow_client = None
38
 
39
+ # ─── 3. RETRY-ENABLED MODEL DOWNLOAD ─────────────────────────────────────────
40
+ @retry(
41
+ stop=stop_after_attempt(3),
42
+ wait=wait_exponential(multiplier=1, min=2, max=10),
43
+ retry=retry_if_exception_type((requests.RequestException, OSError))
44
+ )
45
+ def download_model() -> str:
46
+ """Download model from HuggingFace Hub with retry logic."""
47
+ logger.info(f"πŸš€ Downloading model from {MODEL_REPO}...")
48
+ local_path = hf_hub_download(
49
+ repo_id=MODEL_REPO,
50
+ filename=MODEL_FILENAME,
51
+ cache_dir="/tmp/huggingface"
52
+ )
53
+ logger.info(f"βœ… Model downloaded to {local_path}")
54
+ return local_path
55
 
56
+ def init_roboflow_client():
57
+ """Initialize Roboflow client if API key is available."""
58
+ global roboflow_client
59
+ if API_KEY:
60
+ try:
61
+ roboflow_client = InferenceHTTPClient(
62
+ api_url="https://serverless.roboflow.com",
63
+ api_key=API_KEY
64
+ )
65
+ logger.info("βœ… Roboflow client initialized")
66
+ except Exception as e:
67
+ logger.error(f"❌ Roboflow client initialization failed: {e}")
68
+ roboflow_client = None
69
+ else:
70
+ logger.warning("⚠️ ROBOFLOW_API_KEY not set")
71
 
72
+ # ─── 4. LIFESPAN EVENT HANDLER ───────────────────────────────────────────────
73
+ @asynccontextmanager
74
+ async def lifespan(app: FastAPI):
75
+ """Manage model loading at startup and cleanup at shutdown."""
76
+ global MODEL
77
  try:
78
+ local_model_path = download_model()
79
+ logger.info("πŸ”„ Loading Keras model with JAX backend...")
80
+ MODEL = keras.saving.load_model(local_model_path)
81
+ logger.info(f"βœ… Model loaded successfully! Input shape: {MODEL.input_shape}")
82
 
83
+ # Initialize Roboflow client
84
+ init_roboflow_client()
 
85
  except Exception as e:
86
+ logger.error(f"❌ Startup failed: {e}")
87
+ MODEL = None
88
+
89
+ yield
90
+ logger.info("πŸ›‘ Shutting down...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
+ app = FastAPI(
93
+ title="Teeth Caries Detection API",
94
+ description="Dental anomaly detection using CNN Autoencoder",
95
+ version="1.0.0",
96
+ lifespan=lifespan
97
+ )
98
 
99
+ # ─── 5. ROBOFLOW API CALL ────────────────────────────────────────────────────
100
+ def call_roboflow_api(image_path: str) -> Dict:
101
+ """Call Roboflow workflow using inference SDK."""
102
+ if not roboflow_client:
103
+ raise Exception("Roboflow client not initialized")
104
+
105
+ result = roboflow_client.run_workflow(
106
+ workspace_name=WORKSPACE_NAME,
107
+ workflow_id=WORKFLOW_ID,
108
+ images={"image": image_path},
109
+ use_cache=False
110
+ )
111
+ return result
112
 
113
+ # ─── 6. ROUTES ───────────────────────────────────────────────────────────────
114
+ @app.get("/")
115
+ async def root():
116
  """Health check endpoint."""
117
+ return {
118
+ "status": "active",
119
+ "model_ready": MODEL is not None,
120
+ "backend": os.environ.get("KERAS_BACKEND"),
121
+ "api_configured": API_KEY is not None,
122
+ "roboflow_ready": roboflow_client is not None
123
+ }
124
+
125
+ @app.get("/health")
126
+ async def health():
127
+ """Detailed health check."""
128
+ if MODEL is None:
129
+ return JSONResponse(
130
+ status_code=503,
131
+ content={"status": "unhealthy", "reason": "Model not loaded"}
132
+ )
133
+ return {
134
  "status": "healthy",
135
+ "model_input_shape": str(MODEL.input_shape),
136
+ "roboflow_status": "ready" if roboflow_client else "not configured"
137
+ }
138
 
139
+ @app.post("/analyze")
140
+ async def analyze(file: UploadFile = File(...)):
141
  """
142
+ Analyze dental image for caries detection.
143
+
144
+ Returns anomaly scores for each detected tooth.
145
  """
146
+ if MODEL is None:
147
+ raise HTTPException(status_code=503, detail="Model not initialized. Please retry later.")
148
 
149
+ if not roboflow_client:
150
+ raise HTTPException(status_code=500, detail="ROBOFLOW_API_KEY not configured")
151
+
152
+ # ─── Read and validate image ─────────────────────────────────────────────
153
  try:
154
+ contents = await file.read()
155
+ if len(contents) == 0:
156
+ raise HTTPException(status_code=400, detail="Empty file uploaded")
157
 
158
+ nparr = np.frombuffer(contents, np.uint8)
159
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
 
 
 
 
160
 
161
+ if img is None or img.size == 0:
162
+ raise HTTPException(status_code=400, detail="Invalid image format or corrupted file")
163
 
164
+ img_h, img_w = img.shape[:2]
165
+ if img_h < 50 or img_w < 50:
166
+ raise HTTPException(status_code=400, detail="Image too small (min 50x50 pixels)")
167
+
168
+ except HTTPException:
169
+ raise
170
+ except Exception as e:
171
+ logger.error(f"Image decode error: {e}")
172
+ raise HTTPException(status_code=400, detail=f"Image processing failed: {str(e)}")
173
+
174
+ # ─── Call Roboflow detection ─────────────────────────────────────────────
175
+ tmp_path = None
176
+ try:
177
+ # Save to temp file for inference_sdk
178
+ with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
179
+ tmp.write(contents)
180
+ tmp_path = tmp.name
181
 
182
+ result_json = call_roboflow_api(tmp_path)
 
 
183
 
184
+ # ─── DEBUG: Log what Roboflow returns ────────────────────────────────
 
185
  logger.info(f"πŸ” Response Type: {type(result_json)}")
186
  logger.info(f"πŸ” Response Keys: {result_json.keys() if isinstance(result_json, dict) else 'Not a dict'}")
187
+ logger.info(f"πŸ” Full Response: {result_json}")
188
 
189
+ except Exception as e:
190
+ logger.error(f"Roboflow workflow error: {e}")
191
+ raise HTTPException(status_code=502, detail=f"Detection service failed: {str(e)}")
192
+ finally:
193
+ # Clean up temp file
194
+ if tmp_path and os.path.exists(tmp_path):
195
+ os.remove(tmp_path)
196
+
197
+ # ─── Parse detections ────────────────────────────────────────────────────
198
+ predictions = []
199
+
200
+ try:
201
+ # Handle different response structures
202
+ if isinstance(result_json, list) and len(result_json) > 0:
203
+ # Response is a list
204
+ first_item = result_json[0]
205
+ logger.info(f"πŸ” First item type: {type(first_item)}")
206
+ logger.info(f"πŸ” First item keys: {first_item.keys() if isinstance(first_item, dict) else 'Not dict'}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
+ # FIXED: Check for "predictions" directly first!
209
+ if isinstance(first_item, dict):
210
+ if "predictions" in first_item:
211
+ predictions = first_item["predictions"]
212
+ logger.info(f"βœ… Found predictions directly in first_item")
213
+ elif "outputs" in first_item:
214
+ outputs = first_item["outputs"]
215
+ logger.info(f"πŸ” Found outputs: {type(outputs)}")
216
+ if isinstance(outputs, dict):
217
+ predictions = outputs.get("predictions", [])
218
+
219
+ elif isinstance(result_json, dict):
220
+ # Response is a dict
221
+ logger.info(f"πŸ” Dict keys: {list(result_json.keys())}")
222
 
223
+ # Try "predictions" first
224
+ if "predictions" in result_json:
225
+ predictions = result_json["predictions"]
226
+ elif "outputs" in result_json:
227
+ outputs = result_json["outputs"]
228
+ if isinstance(outputs, list) and len(outputs) > 0:
229
+ output_data = outputs[0]
230
+ elif isinstance(outputs, dict):
231
+ output_data = outputs
232
+ else:
233
+ output_data = {}
234
+ predictions = output_data.get("predictions", [])
 
 
 
 
 
 
 
235
 
236
+ # Handle case where predictions might be a dict
237
+ if isinstance(predictions, dict) and "predictions" in predictions:
238
+ predictions = predictions["predictions"]
239
 
240
+ logger.info(f"πŸ” Final predictions type: {type(predictions)}")
241
+ logger.info(f"πŸ” Final predictions count: {len(predictions) if isinstance(predictions, list) else 0}")
242
+ if isinstance(predictions, list) and len(predictions) > 0:
243
+ logger.info(f"πŸ” First prediction keys: {predictions[0].keys()}")
244
+
245
+ except Exception as e:
246
+ logger.error(f"❌ Parsing error: {e}", exc_info=True)
247
+ predictions = []
248
+
249
+ if not predictions or (isinstance(predictions, list) and len(predictions) == 0):
250
+ logger.warning("⚠️ No predictions found after parsing")
251
+ return {
252
+ "status": "success",
253
+ "tooth_count": 0,
254
+ "results": [],
255
+ "message": "No teeth detected in image"
256
+ }
257
+
258
+ logger.info(f"βœ… Found {len(predictions)} teeth detections")
259
+
260
+
261
+ # ─── Process crops ───────────────────────────────────────────────────────
262
+ batch_images = []
263
+ metadata = []
264
+
265
+ for i, det in enumerate(predictions[:12]):
266
+ try:
267
+ xc, yc, w, h = det["x"], det["y"], det["width"], det["height"]
268
+ x1 = max(0, int(xc - w/2))
269
+ y1 = max(0, int(yc - h/2))
270
+ x2 = min(img_w, int(xc + w/2))
271
+ y2 = min(img_h, int(yc + h/2))
272
+
273
+ if x2 <= x1 or y2 <= y1:
274
  continue
275
+
276
+ crop = img[y1:y2, x1:x2]
277
+ if crop.size == 0:
278
+ continue
279
+
280
+ # Preprocessing pipeline
281
+ gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)
282
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
283
+ gray = clahe.apply(gray)
284
+
285
+ # Aspect-ratio preserving resize with padding
286
+ h_c, w_c = gray.shape
287
+ scale = IMG_SIZE / max(h_c, w_c)
288
+ nw, nh = int(w_c * scale), int(h_c * scale)
289
+ resized = cv2.resize(gray, (nw, nh), interpolation=cv2.INTER_AREA)
290
+
291
+ canvas = np.zeros((IMG_SIZE, IMG_SIZE), dtype=np.uint8)
292
+ y_offset = (IMG_SIZE - nh) // 2
293
+ x_offset = (IMG_SIZE - nw) // 2
294
+ canvas[y_offset:y_offset + nh, x_offset:x_offset + nw] = resized
295
+
296
+ final_img = (canvas.astype(np.float32) / 255.0)[..., np.newaxis]
297
+ batch_images.append(final_img)
298
+ metadata.append({"id": i, "box": [x1, y1, x2, y2]})
299
+
300
+ logger.info(f"βœ… Processed tooth {i}: bbox=[{x1},{y1},{x2},{y2}]")
301
+
302
+ except Exception as e:
303
+ logger.warning(f"⚠️ Skipping detection {i} due to error: {e}")
304
+ continue
305
+
306
+ if not batch_images:
307
+ return {
308
  "status": "success",
309
+ "tooth_count": 0,
310
+ "results": [],
311
+ "message": "No valid tooth crops extracted"
 
 
 
 
312
  }
313
+
314
+ logger.info(f"βœ… Prepared {len(batch_images)} tooth images for inference")
315
+
316
+ # ─── Model inference ─────────────────────────────────────────────────────
317
+ try:
318
+ batch_array = np.array(batch_images)
319
+ logger.info(f"πŸ”„ Running inference on batch shape: {batch_array.shape}")
320
+ reconstructions = MODEL.predict(batch_array, verbose=0)
321
+ errors = np.mean(np.abs(batch_array - reconstructions), axis=(1, 2, 3))
322
+ logger.info(f"βœ… Inference complete. Scores: {errors}")
 
323
  except Exception as e:
324
+ logger.error(f"❌ Inference error: {e}")
325
+ raise HTTPException(status_code=500, detail="Model inference failed")
 
 
 
 
 
 
 
 
 
326
 
327
+ # ─── Format results ──────────────────────────────────────────────────────
328
+ results = []
329
+ for idx, score in enumerate(errors):
330
+ score_val = float(score)
331
+ results.append({
332
+ "tooth_index": metadata[idx]["id"],
333
+ "anomaly_score": round(score_val, 4),
334
+ "is_caries_detected": score_val > THRESHOLD,
335
+ "bounding_box": metadata[idx]["box"],
336
+ "confidence": "high" if score_val > THRESHOLD * 1.5 else "medium" if score_val > THRESHOLD else "low"
337
+ })
338
+
339
+ logger.info(f"πŸŽ‰ Analysis complete: {len(results)} teeth analyzed")
340
+
341
+ return {
342
+ "status": "success",
343
+ "tooth_count": len(results),
344
+ "detection_threshold": THRESHOLD,
345
+ "results": results
346
+ }
347
 
348
+ # ─── 7. ERROR HANDLER ────────────────────────────────────────────────────────
349
+ @app.exception_handler(Exception)
350
+ async def global_exception_handler(request, exc):
351
+ logger.error(f"Unhandled exception: {exc}", exc_info=True)
352
+ return JSONResponse(
353
+ status_code=500,
354
+ content={"detail": "Internal server error occurred"}
355
  )