sairamv1 commited on
Commit
deb0b65
Β·
verified Β·
1 Parent(s): 34dd9a6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +219 -323
app.py CHANGED
@@ -1,368 +1,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 keys: {first_item.keys() if isinstance(first_item, dict) else 'Not dict'}")
206
-
207
- if "outputs" in first_item:
208
- outputs = first_item["outputs"]
209
- logger.info(f"πŸ” Outputs type: {type(outputs)}")
210
- logger.info(f"πŸ” Outputs content: {outputs}")
211
-
212
- # Try to get predictions from outputs
213
- if isinstance(outputs, dict):
214
- predictions = outputs.get("predictions", outputs.get("image", {}).get("predictions", []))
215
- elif isinstance(outputs, list):
216
- predictions = outputs
217
-
218
- elif isinstance(result_json, dict):
219
- # Response is a dict
220
- logger.info(f"πŸ” Dict keys: {list(result_json.keys())}")
221
-
222
- # Try multiple paths
223
- if "outputs" in result_json:
224
- outputs = result_json["outputs"]
225
- logger.info(f"πŸ” Found outputs, type: {type(outputs)}")
226
 
227
- if isinstance(outputs, list) and len(outputs) > 0:
228
- output_data = outputs[0]
229
- elif isinstance(outputs, dict):
230
- output_data = outputs
231
- else:
232
- output_data = {}
233
-
234
- logger.info(f"πŸ” Output data: {output_data}")
 
 
 
 
 
 
235
 
236
- # Extract predictions
237
- if isinstance(output_data, dict):
 
 
 
 
 
 
 
 
 
238
  predictions = output_data.get("predictions", [])
239
-
240
- elif "predictions" in result_json:
241
- predictions = result_json["predictions"]
 
 
 
 
 
 
242
 
243
- elif "image" in result_json:
244
- predictions = result_json["image"].get("predictions", [])
 
245
 
246
- # Handle nested predictions dict
247
- if isinstance(predictions, dict):
248
- logger.info(f"πŸ” Predictions is dict with keys: {predictions.keys()}")
249
- if "predictions" in predictions:
250
- predictions = predictions["predictions"]
251
- elif "image" in predictions:
252
- predictions = predictions["image"].get("predictions", [])
 
253
 
254
- logger.info(f"πŸ” Final predictions type: {type(predictions)}")
255
- logger.info(f"πŸ” Final predictions length: {len(predictions) if isinstance(predictions, list) else 'Not a list'}")
256
- if isinstance(predictions, list) and len(predictions) > 0:
257
- logger.info(f"πŸ” First prediction sample: {predictions[0]}")
258
-
259
- except Exception as e:
260
- logger.error(f"❌ Parsing error: {e}", exc_info=True)
261
- predictions = []
262
-
263
- if not predictions or (isinstance(predictions, list) and len(predictions) == 0):
264
- logger.warning("⚠️ No predictions found after parsing")
265
- return {
266
- "status": "success",
267
- "tooth_count": 0,
268
- "results": [],
269
- "message": "No teeth detected in image"
270
- }
271
-
272
- logger.info(f"βœ… Found {len(predictions)} teeth detections")
273
-
274
- # ─── Process crops ───────────────────────────────────────────────────────
275
- batch_images = []
276
- metadata = []
277
-
278
- for i, det in enumerate(predictions[:12]):
279
- try:
280
- xc, yc, w, h = det["x"], det["y"], det["width"], det["height"]
281
- x1 = max(0, int(xc - w/2))
282
- y1 = max(0, int(yc - h/2))
283
- x2 = min(img_w, int(xc + w/2))
284
- y2 = min(img_h, int(yc + h/2))
285
-
286
- if x2 <= x1 or y2 <= y1:
287
- continue
288
-
289
- crop = img[y1:y2, x1:x2]
290
- if crop.size == 0:
291
  continue
292
-
293
- # Preprocessing pipeline
294
- gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)
295
- clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
296
- gray = clahe.apply(gray)
297
-
298
- # Aspect-ratio preserving resize with padding
299
- h_c, w_c = gray.shape
300
- scale = IMG_SIZE / max(h_c, w_c)
301
- nw, nh = int(w_c * scale), int(h_c * scale)
302
- resized = cv2.resize(gray, (nw, nh), interpolation=cv2.INTER_AREA)
303
-
304
- canvas = np.zeros((IMG_SIZE, IMG_SIZE), dtype=np.uint8)
305
- y_offset = (IMG_SIZE - nh) // 2
306
- x_offset = (IMG_SIZE - nw) // 2
307
- canvas[y_offset:y_offset + nh, x_offset:x_offset + nw] = resized
308
-
309
- final_img = (canvas.astype(np.float32) / 255.0)[..., np.newaxis]
310
- batch_images.append(final_img)
311
- metadata.append({"id": i, "box": [x1, y1, x2, y2]})
312
-
313
- logger.info(f"βœ… Processed tooth {i}: bbox=[{x1},{y1},{x2},{y2}]")
314
-
315
- except Exception as e:
316
- logger.warning(f"⚠️ Skipping detection {i} due to error: {e}")
317
- continue
318
-
319
- if not batch_images:
320
- return {
321
  "status": "success",
322
- "tooth_count": 0,
323
- "results": [],
324
- "message": "No valid tooth crops extracted"
 
 
 
 
325
  }
326
-
327
- logger.info(f"βœ… Prepared {len(batch_images)} tooth images for inference")
328
-
329
- # ─── Model inference ─────────────────────────────────────────────────────
330
- try:
331
- batch_array = np.array(batch_images)
332
- logger.info(f"πŸ”„ Running inference on batch shape: {batch_array.shape}")
333
- reconstructions = MODEL.predict(batch_array, verbose=0)
334
- errors = np.mean(np.abs(batch_array - reconstructions), axis=(1, 2, 3))
335
- logger.info(f"βœ… Inference complete. Scores: {errors}")
 
336
  except Exception as e:
337
- logger.error(f"❌ Inference error: {e}")
338
- raise HTTPException(status_code=500, detail="Model inference failed")
339
-
340
- # ─── Format results ──────────────────────────────────────────────────────
341
- results = []
342
- for idx, score in enumerate(errors):
343
- score_val = float(score)
344
- results.append({
345
- "tooth_index": metadata[idx]["id"],
346
- "anomaly_score": round(score_val, 4),
347
- "is_caries_detected": score_val > THRESHOLD,
348
- "bounding_box": metadata[idx]["box"],
349
- "confidence": "high" if score_val > THRESHOLD * 1.5 else "medium" if score_val > THRESHOLD else "low"
350
- })
351
 
352
- logger.info(f"πŸŽ‰ Analysis complete: {len(results)} teeth analyzed")
353
-
354
- return {
355
- "status": "success",
356
- "tooth_count": len(results),
357
- "detection_threshold": THRESHOLD,
358
- "results": results
359
- }
360
 
361
- # ─── 7. ERROR HANDLER ────────────────────────────────────────────────────────
362
- @app.exception_handler(Exception)
363
- async def global_exception_handler(request, exc):
364
- logger.error(f"Unhandled exception: {exc}", exc_info=True)
365
- return JSONResponse(
366
- status_code=500,
367
- content={"detail": "Internal server error occurred"}
368
  )
 
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
  )