MohsinElis commited on
Commit
e38baa3
·
verified ·
1 Parent(s): 8de36d4

feat: add model=ensemble route and in-memory SHA-256 LRU cache

Browse files
Files changed (1) hide show
  1. api/main.py +48 -12
api/main.py CHANGED
@@ -9,6 +9,8 @@ FastAPI service providing:
9
 
10
  import io
11
  import os
 
 
12
  from datetime import datetime, timezone
13
  from pathlib import Path
14
  from typing import Dict, List, Optional
@@ -20,14 +22,18 @@ from fastapi.middleware.cors import CORSMiddleware
20
  from fastapi.responses import JSONResponse
21
 
22
  from .schemas import HealthResponse, ModelsResponse, PredictionResponse, ModelInfo
23
- from .model_server import TamperingModelServer
24
 
25
  MODEL_INFO = {
26
- "tiny": {"params": "563K", "description": "Fastest - edge/mobile tampering detection profile"},
27
- "base": {"params": "3.28M", "description": "Balanced accuracy, splicing detection, and localization"},
28
- "large": {"params": "14.78M","description": "Highest forensic precision for inpainting & composite extortion"},
 
29
  }
30
 
 
 
 
31
  model_registry: Dict[str, TamperingModelServer] = {}
32
  DEFAULT_VARIANT = os.environ.get("PATCH_MFFT_VARIANT", "base")
33
  IMG_SIZE = int(os.environ.get("PATCH_MFFT_IMG_SIZE", "384"))
@@ -87,12 +93,17 @@ async def health():
87
 
88
  @app.get("/models", response_model=ModelsResponse)
89
  async def list_models():
 
 
 
 
 
90
  return ModelsResponse(
91
- default=DEFAULT_VARIANT if DEFAULT_VARIANT in model_registry else list(model_registry.keys())[0],
92
  models=[
93
  ModelInfo(
94
  id=v,
95
- loaded=v in model_registry,
96
  params=MODEL_INFO[v]["params"],
97
  description=MODEL_INFO[v]["description"],
98
  )
@@ -104,13 +115,14 @@ async def list_models():
104
  @app.post("/predict", response_model=PredictionResponse)
105
  async def predict(
106
  file: UploadFile = File(...),
107
- model: str = "large",
108
  ):
109
- variant = (model or DEFAULT_VARIANT).lower()
110
- if variant not in model_registry:
 
111
  raise HTTPException(
112
  status_code=404,
113
- detail=f"Model variant '{variant}' not found. Available: {list(model_registry.keys())}",
114
  )
115
 
116
  if not file.content_type or not file.content_type.startswith("image/"):
@@ -120,12 +132,36 @@ async def predict(
120
  if len(content) > 25 * 1024 * 1024:
121
  raise HTTPException(status_code=400, detail="Image size exceeds 25 MB limit.")
122
 
 
 
 
 
 
 
 
 
 
 
123
  try:
124
  image = Image.open(io.BytesIO(content)).convert("RGB")
125
  except Exception:
126
  raise HTTPException(status_code=400, detail="Corrupted image file.")
127
 
128
- server = model_registry[variant]
129
- result = server.predict(image, filename=file.filename)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  return PredictionResponse(**result)
 
9
 
10
  import io
11
  import os
12
+ import hashlib
13
+ from collections import OrderedDict
14
  from datetime import datetime, timezone
15
  from pathlib import Path
16
  from typing import Dict, List, Optional
 
22
  from fastapi.responses import JSONResponse
23
 
24
  from .schemas import HealthResponse, ModelsResponse, PredictionResponse, ModelInfo
25
+ from .model_server import TamperingModelServer, predict_ensemble
26
 
27
  MODEL_INFO = {
28
+ "tiny": {"params": "563K", "description": "Fastest - edge/mobile tampering detection profile (~4 ms)"},
29
+ "base": {"params": "3.28M", "description": "Standard Profile - balanced accuracy and localized inpainting"},
30
+ "large": {"params": "13.94M", "description": "Deep Profile - recommended for vintage/retro AI & degraded media"},
31
+ "ensemble": {"params": "17.22M", "description": "SOTA Production Ensemble - Conservative Fusion of Base + Large with Heatmap Max-Gating"},
32
  }
33
 
34
+ PREDICTION_CACHE: OrderedDict = OrderedDict()
35
+ MAX_CACHE_SIZE = 256
36
+
37
  model_registry: Dict[str, TamperingModelServer] = {}
38
  DEFAULT_VARIANT = os.environ.get("PATCH_MFFT_VARIANT", "base")
39
  IMG_SIZE = int(os.environ.get("PATCH_MFFT_IMG_SIZE", "384"))
 
93
 
94
  @app.get("/models", response_model=ModelsResponse)
95
  async def list_models():
96
+ default_model = "ensemble" if ("base" in model_registry and "large" in model_registry) else (
97
+ DEFAULT_VARIANT if DEFAULT_VARIANT in model_registry else (
98
+ list(model_registry.keys())[0] if model_registry else "base"
99
+ )
100
+ )
101
  return ModelsResponse(
102
+ default=default_model,
103
  models=[
104
  ModelInfo(
105
  id=v,
106
+ loaded=(v in model_registry) or (v == "ensemble" and "base" in model_registry and "large" in model_registry),
107
  params=MODEL_INFO[v]["params"],
108
  description=MODEL_INFO[v]["description"],
109
  )
 
115
  @app.post("/predict", response_model=PredictionResponse)
116
  async def predict(
117
  file: UploadFile = File(...),
118
+ model: str = "ensemble",
119
  ):
120
+ variant = (model or "ensemble").lower()
121
+ valid_variants = list(model_registry.keys()) + (["ensemble"] if "base" in model_registry and "large" in model_registry else [])
122
+ if variant not in valid_variants:
123
  raise HTTPException(
124
  status_code=404,
125
+ detail=f"Model variant '{variant}' not found. Available: {valid_variants}",
126
  )
127
 
128
  if not file.content_type or not file.content_type.startswith("image/"):
 
132
  if len(content) > 25 * 1024 * 1024:
133
  raise HTTPException(status_code=400, detail="Image size exceeds 25 MB limit.")
134
 
135
+ sha256_hash = hashlib.sha256(content).hexdigest()
136
+ cache_key = f"{sha256_hash}_{variant}"
137
+
138
+ # Instant In-Memory LRU Cache Check (< 1 ms latency)
139
+ if cache_key in PREDICTION_CACHE:
140
+ cached_result = dict(PREDICTION_CACHE[cache_key])
141
+ cached_result["cached"] = True
142
+ PREDICTION_CACHE.move_to_end(cache_key)
143
+ return PredictionResponse(**cached_result)
144
+
145
  try:
146
  image = Image.open(io.BytesIO(content)).convert("RGB")
147
  except Exception:
148
  raise HTTPException(status_code=400, detail="Corrupted image file.")
149
 
150
+ if variant == "ensemble":
151
+ result = predict_ensemble(
152
+ model_registry["base"],
153
+ model_registry["large"],
154
+ image,
155
+ filename=file.filename,
156
+ sha256_hash=sha256_hash,
157
+ )
158
+ else:
159
+ server = model_registry[variant]
160
+ result = server.predict(image, filename=file.filename, sha256_hash=sha256_hash)
161
+
162
+ # Save to high-speed LRU memory cache
163
+ PREDICTION_CACHE[cache_key] = result
164
+ if len(PREDICTION_CACHE) > MAX_CACHE_SIZE:
165
+ PREDICTION_CACHE.popitem(last=False)
166
 
167
  return PredictionResponse(**result)