doniramdani820 commited on
Commit
fa25762
Β·
verified Β·
1 Parent(s): 83e4189

Upload 6 files

Browse files
Files changed (2) hide show
  1. README.md +1 -0
  2. app.py +153 -15
README.md CHANGED
@@ -151,3 +151,4 @@ MIT License - See LICENSE file for details
151
 
152
  **Note:** This API is for educational and research purposes only. Use responsibly and respect website terms of service.
153
 
 
 
151
 
152
  **Note:** This API is for educational and research purposes only. Use responsibly and respect website terms of service.
153
 
154
+
app.py CHANGED
@@ -14,11 +14,17 @@ import io
14
  from PIL import Image
15
  import time
16
  import os
17
- from functools import lru_cache
18
 
19
  app = Flask(__name__)
20
  CORS(app) # Enable CORS for browser extension
21
 
 
 
 
 
 
 
22
  # Global variables
23
  model = None
24
  class_names = None
@@ -27,6 +33,7 @@ request_count = 0
27
  successful_count = 0
28
  failed_count = 0
29
  total_latency = 0.0
 
30
 
31
  # Configuration
32
  MODEL_FOLDER = "." # Models in root folder (no subfolder)
@@ -34,6 +41,50 @@ CONFIDENCE_THRESHOLD = 0.25
34
  IOU_THRESHOLD = 0.45
35
  MASK_THRESHOLD_PERCENTAGE = 1.0 # 1% minimum overlap
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  print("="*60)
38
  print("πŸš€ reCAPTCHA 4x4 Segmentation API")
39
  print("="*60)
@@ -130,30 +181,112 @@ def run_segmentation(img):
130
 
131
 
132
  def normalize_text(text):
133
- """Normalize challenge text"""
 
 
 
 
 
134
  text = text.lower().strip()
135
 
136
- # Singular/plural mapping
 
 
 
 
 
 
137
  mappings = {
 
 
 
 
 
 
 
138
  'bicycle': 'bicycles',
 
 
 
 
139
  'bus': 'buses',
 
 
140
  'car': 'cars',
141
- 'fire hydrant': 'fire hydrants',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  'motorcycle': 'motorcycles',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  'traffic light': 'traffic lights',
144
- 'crosswalk': 'crosswalks',
145
- 'vehicle': 'vehicles',
146
- 'bridge': 'bridges',
 
147
  'boat': 'boats',
148
- 'taxi': 'taxis',
149
- 'stair': 'stairs',
150
- 'chimney': 'chimneys'
 
 
 
 
 
 
 
 
 
 
 
 
151
  }
152
 
153
- for singular, plural in mappings.items():
154
- if singular in text:
155
- return plural
 
 
 
 
 
156
 
 
157
  return text
158
 
159
 
@@ -176,6 +309,8 @@ def get_tiles_to_click(boxes, scores, class_ids, masks, challenge_title, img_wid
176
  for i, (box, score, class_id, mask) in enumerate(zip(boxes, scores, class_ids, masks)):
177
  # Get class name
178
  det_class = class_names[class_id].lower()
 
 
179
 
180
  # Check if detection matches challenge
181
  if normalized_title not in det_class and det_class not in normalized_title:
@@ -211,21 +346,24 @@ def get_tiles_to_click(boxes, scores, class_ids, masks, challenge_title, img_wid
211
 
212
  @app.route('/health', methods=['GET'])
213
  def health():
214
- """Health check endpoint"""
215
  return jsonify({
216
  'status': 'healthy',
217
  'model_loaded': model is not None,
 
218
  'model_load_time_s': model_load_time,
219
  'requests_total': request_count,
220
  'requests_successful': successful_count,
221
  'requests_failed': failed_count,
 
222
  'avg_latency_s': total_latency / max(request_count, 1)
223
  })
224
 
225
 
226
  @app.route('/predict', methods=['POST'])
 
227
  def predict():
228
- """Main prediction endpoint"""
229
  global request_count, successful_count, failed_count, total_latency
230
 
231
  start_time = time.time()
 
14
  from PIL import Image
15
  import time
16
  import os
17
+ from functools import lru_cache, wraps
18
 
19
  app = Flask(__name__)
20
  CORS(app) # Enable CORS for browser extension
21
 
22
+ # API Security - Load secret key from environment
23
+ API_SECRET_KEY = os.environ.get('API_SECRET_KEY', None)
24
+ print(f"\nπŸ” API Security: {'ENABLED' if API_SECRET_KEY else 'DISABLED (PUBLIC ACCESS)'}")
25
+ if API_SECRET_KEY:
26
+ print(f" Secret Key: {API_SECRET_KEY[:8]}{'*' * (len(API_SECRET_KEY) - 8)}")
27
+
28
  # Global variables
29
  model = None
30
  class_names = None
 
33
  successful_count = 0
34
  failed_count = 0
35
  total_latency = 0.0
36
+ unauthorized_count = 0
37
 
38
  # Configuration
39
  MODEL_FOLDER = "." # Models in root folder (no subfolder)
 
41
  IOU_THRESHOLD = 0.45
42
  MASK_THRESHOLD_PERCENTAGE = 1.0 # 1% minimum overlap
43
 
44
+
45
+ def require_api_key(f):
46
+ """
47
+ Decorator to require API key authentication
48
+
49
+ If API_SECRET_KEY is set in environment:
50
+ - Check X-API-Key header matches secret
51
+ - Return 401 if missing or invalid
52
+
53
+ If API_SECRET_KEY is NOT set:
54
+ - Allow all requests (public access)
55
+ """
56
+ @wraps(f)
57
+ def decorated_function(*args, **kwargs):
58
+ global unauthorized_count
59
+
60
+ # If no secret key configured, allow all requests
61
+ if API_SECRET_KEY is None:
62
+ return f(*args, **kwargs)
63
+
64
+ # Check for API key in header
65
+ provided_key = request.headers.get('X-API-Key')
66
+
67
+ if not provided_key:
68
+ unauthorized_count += 1
69
+ return jsonify({
70
+ 'success': False,
71
+ 'error': 'Missing API key',
72
+ 'message': 'Please provide X-API-Key header'
73
+ }), 401
74
+
75
+ if provided_key != API_SECRET_KEY:
76
+ unauthorized_count += 1
77
+ return jsonify({
78
+ 'success': False,
79
+ 'error': 'Invalid API key',
80
+ 'message': 'The provided API key is incorrect'
81
+ }), 401
82
+
83
+ # Valid key, proceed with request
84
+ return f(*args, **kwargs)
85
+
86
+ return decorated_function
87
+
88
  print("="*60)
89
  print("πŸš€ reCAPTCHA 4x4 Segmentation API")
90
  print("="*60)
 
181
 
182
 
183
  def normalize_text(text):
184
+ """
185
+ Normalize challenge text with comprehensive synonym mapping
186
+
187
+ Maps: reCAPTCHA challenge text β†’ Model class names
188
+ Handles: singular/plural, synonyms, articles
189
+ """
190
  text = text.lower().strip()
191
 
192
+ # Remove articles "a " and "the " for better matching
193
+ text = text.replace('a ', '').replace('the ', '')
194
+
195
+ # Comprehensive mapping: challenge_text β†’ model_class (4x4)
196
+ # Model classes: 'a fire hydrant', 'bicycles', 'buses', 'cars', 'chimneys',
197
+ # 'crosswalks', 'motorcycles', 'parking meters', 'stairs',
198
+ # 'taxis', 'tractors', 'traffic lights'
199
  mappings = {
200
+ # Fire hydrant (model: "fire hydrant" - already removed "a ")
201
+ 'fire hydrant': 'fire hydrant',
202
+ 'fire hydrants': 'fire hydrant',
203
+ 'hydrant': 'fire hydrant',
204
+ 'hydrants': 'fire hydrant',
205
+
206
+ # Bicycle β†’ bicycles (model: "bicycles")
207
  'bicycle': 'bicycles',
208
+ 'bike': 'bicycles',
209
+ 'bikes': 'bicycles',
210
+
211
+ # Bus β†’ buses (model: "buses")
212
  'bus': 'buses',
213
+
214
+ # Car β†’ cars (model: "cars")
215
  'car': 'cars',
216
+ 'vehicle': 'cars',
217
+ 'vehicles': 'cars',
218
+ 'automobile': 'cars',
219
+ 'automobiles': 'cars',
220
+ 'taxi': 'taxis', # Model has taxis
221
+ 'cab': 'taxis',
222
+ 'cabs': 'taxis',
223
+
224
+ # Chimney β†’ chimneys (model: "chimneys")
225
+ 'chimney': 'chimneys',
226
+
227
+ # Crosswalk β†’ crosswalks (model: "crosswalks")
228
+ 'crosswalk': 'crosswalks',
229
+ 'pedestrian crossing': 'crosswalks',
230
+ 'zebra crossing': 'crosswalks',
231
+
232
+ # Motorcycle β†’ motorcycles (model: "motorcycles")
233
  'motorcycle': 'motorcycles',
234
+ 'motorbike': 'motorcycles',
235
+ 'motorbikes': 'motorcycles',
236
+
237
+ # Parking meters (model: "parking meters")
238
+ 'parking meter': 'parking meters',
239
+ 'parking metre': 'parking meters',
240
+ 'parking metres': 'parking meters',
241
+
242
+ # IMPORTANT: Ladder β†’ Stairs (model: "stairs")
243
+ 'ladder': 'stairs',
244
+ 'ladders': 'stairs',
245
+ 'stair': 'stairs',
246
+ 'staircase': 'stairs',
247
+ 'staircases': 'stairs',
248
+ 'step': 'stairs',
249
+ 'steps': 'stairs',
250
+
251
+ # IMPORTANT: Tractor β†’ Tractors (model: "tractors" plural!)
252
+ 'tractor': 'tractors',
253
+ 'farm tractor': 'tractors',
254
+ 'farm tractors': 'tractors',
255
+
256
+ # Traffic light β†’ traffic lights (model: "traffic lights")
257
  'traffic light': 'traffic lights',
258
+ 'traffic signal': 'traffic lights',
259
+ 'traffic signals': 'traffic lights',
260
+
261
+ # Boat variations (not in 4x4, but keep for fallback)
262
  'boat': 'boats',
263
+ 'boats': 'boats',
264
+ 'ship': 'boats',
265
+ 'ships': 'boats',
266
+
267
+ # Bridge variations (not in 4x4, but keep for fallback)
268
+ 'bridge': 'bridges',
269
+ 'bridges': 'bridges',
270
+
271
+ # Tree/Palm variations (not in 4x4 model!)
272
+ 'tree': 'trees',
273
+ 'trees': 'trees',
274
+ 'palm': 'trees',
275
+ 'palms': 'trees',
276
+ 'palm tree': 'trees',
277
+ 'palm trees': 'trees'
278
  }
279
 
280
+ # Check for exact matches first
281
+ if text in mappings:
282
+ return mappings[text]
283
+
284
+ # Check for partial matches (e.g., "palm trees" contains "palm")
285
+ for challenge_variant, model_class in mappings.items():
286
+ if challenge_variant in text or text in challenge_variant:
287
+ return model_class
288
 
289
+ # No mapping found, return as-is
290
  return text
291
 
292
 
 
309
  for i, (box, score, class_id, mask) in enumerate(zip(boxes, scores, class_ids, masks)):
310
  # Get class name
311
  det_class = class_names[class_id].lower()
312
+ # Also remove articles from detection class for consistent matching
313
+ det_class = det_class.replace('a ', '').replace('the ', '')
314
 
315
  # Check if detection matches challenge
316
  if normalized_title not in det_class and det_class not in normalized_title:
 
346
 
347
  @app.route('/health', methods=['GET'])
348
  def health():
349
+ """Health check endpoint (public - no API key required)"""
350
  return jsonify({
351
  'status': 'healthy',
352
  'model_loaded': model is not None,
353
+ 'security_enabled': API_SECRET_KEY is not None,
354
  'model_load_time_s': model_load_time,
355
  'requests_total': request_count,
356
  'requests_successful': successful_count,
357
  'requests_failed': failed_count,
358
+ 'requests_unauthorized': unauthorized_count,
359
  'avg_latency_s': total_latency / max(request_count, 1)
360
  })
361
 
362
 
363
  @app.route('/predict', methods=['POST'])
364
+ @require_api_key # πŸ” Require API key for prediction
365
  def predict():
366
+ """Main prediction endpoint (protected by API key)"""
367
  global request_count, successful_count, failed_count, total_latency
368
 
369
  start_time = time.time()