CrimsonREwind commited on
Commit
a692076
·
verified ·
1 Parent(s): 1670b90

Upload 22 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Install system dependencies for face_recognition
4
+ RUN apt-get update && apt-get install -y \
5
+ build-essential \
6
+ cmake \
7
+ libopenblas-dev \
8
+ liblapack-dev \
9
+ libx11-dev \
10
+ libgtk-3-dev \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ WORKDIR /app
14
+
15
+ # Install Python dependencies
16
+ COPY requirements.txt .
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy application code
20
+ COPY . .
21
+
22
+ # Expose port
23
+ EXPOSE 5000
24
+
25
+ # Run with gunicorn
26
+ CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "2", "--timeout", "120", "run:app"]
app/__init__.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Flask application factory."""
2
+ import os
3
+ from flask import Flask
4
+ from flask_cors import CORS
5
+ from flask_pymongo import PyMongo
6
+
7
+ from config import config
8
+
9
+ mongo = PyMongo()
10
+
11
+
12
+ def create_app(config_name=None):
13
+ """Create and configure the Flask application."""
14
+ if config_name is None:
15
+ config_name = os.environ.get('FLASK_ENV', 'development')
16
+
17
+ app = Flask(__name__)
18
+ app.config.from_object(config[config_name])
19
+
20
+ # Initialize extensions
21
+ CORS(app, resources={
22
+ r"/api/*": {
23
+ "origins": ["http://localhost:3000", "http://localhost:5173"],
24
+ "methods": ["GET", "POST", "PUT", "DELETE", "PATCH"],
25
+ "allow_headers": ["Content-Type", "Authorization"]
26
+ }
27
+ })
28
+
29
+ mongo.init_app(app)
30
+
31
+ # Initialize Cloudinary
32
+ from app.services.cloudinary_service import init_cloudinary
33
+ init_cloudinary(app.config)
34
+
35
+ # Register blueprints
36
+ from app.routes.people import people_bp
37
+ from app.routes.images import images_bp
38
+ from app.routes.stats import stats_bp
39
+
40
+ app.register_blueprint(people_bp, url_prefix='/api/people')
41
+ app.register_blueprint(images_bp, url_prefix='/api/images')
42
+ app.register_blueprint(stats_bp, url_prefix='/api/stats')
43
+
44
+ # Health check route
45
+ @app.route('/api/health')
46
+ def health_check():
47
+ return {'status': 'healthy', 'message': 'Image Organizer API is running'}
48
+
49
+ return app
app/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (1.53 kB). View file
 
app/__pycache__/models.cpython-310.pyc ADDED
Binary file (2.28 kB). View file
 
app/models.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Database models and schemas."""
2
+ from datetime import datetime
3
+ from bson import ObjectId
4
+
5
+
6
+ class PersonModel:
7
+ """Person model for MongoDB."""
8
+
9
+ @staticmethod
10
+ def create_person(name, face_encoding=None, thumbnail_url=None):
11
+ """Create a new person document."""
12
+ return {
13
+ 'name': name,
14
+ 'face_encodings': [face_encoding] if face_encoding else [],
15
+ 'thumbnail_url': thumbnail_url,
16
+ 'image_count': 0,
17
+ 'created_at': datetime.utcnow(),
18
+ 'updated_at': datetime.utcnow()
19
+ }
20
+
21
+ @staticmethod
22
+ def to_response(person):
23
+ """Convert person document to API response."""
24
+ return {
25
+ 'id': str(person['_id']),
26
+ 'name': person['name'],
27
+ 'thumbnail_url': person.get('thumbnail_url'),
28
+ 'image_count': person.get('image_count', 0),
29
+ 'created_at': person['created_at'].isoformat() if person.get('created_at') else None,
30
+ 'updated_at': person['updated_at'].isoformat() if person.get('updated_at') else None
31
+ }
32
+
33
+
34
+ class ImageModel:
35
+ """Image model for MongoDB."""
36
+
37
+ @staticmethod
38
+ def create_image(cloudinary_url, cloudinary_public_id, original_filename,
39
+ face_encodings=None, person_id=None, face_locations=None):
40
+ """Create a new image document."""
41
+ return {
42
+ 'cloudinary_url': cloudinary_url,
43
+ 'cloudinary_public_id': cloudinary_public_id,
44
+ 'original_filename': original_filename,
45
+ 'face_encodings': face_encodings or [],
46
+ 'face_locations': face_locations or [],
47
+ 'person_id': ObjectId(person_id) if person_id else None,
48
+ 'has_face': bool(face_encodings and len(face_encodings) > 0),
49
+ 'is_identified': person_id is not None,
50
+ 'created_at': datetime.utcnow(),
51
+ 'updated_at': datetime.utcnow()
52
+ }
53
+
54
+ @staticmethod
55
+ def to_response(image, include_person=False):
56
+ """Convert image document to API response."""
57
+ response = {
58
+ 'id': str(image['_id']),
59
+ 'url': image['cloudinary_url'],
60
+ 'original_filename': image.get('original_filename'),
61
+ 'has_face': image.get('has_face', False),
62
+ 'is_identified': image.get('is_identified', False),
63
+ 'face_count': len(image.get('face_locations', [])),
64
+ 'person_id': str(image['person_id']) if image.get('person_id') else None,
65
+ 'created_at': image['created_at'].isoformat() if image.get('created_at') else None
66
+ }
67
+
68
+ if include_person and image.get('person'):
69
+ response['person'] = PersonModel.to_response(image['person'])
70
+
71
+ return response
app/routes/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """Routes package."""
2
+ from .people import people_bp
3
+ from .images import images_bp
4
+ from .stats import stats_bp
5
+
6
+ __all__ = ['people_bp', 'images_bp', 'stats_bp']
app/routes/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (344 Bytes). View file
 
app/routes/__pycache__/images.cpython-310.pyc ADDED
Binary file (8.66 kB). View file
 
app/routes/__pycache__/people.cpython-310.pyc ADDED
Binary file (5.54 kB). View file
 
app/routes/__pycache__/stats.cpython-310.pyc ADDED
Binary file (3.3 kB). View file
 
app/routes/images.py ADDED
@@ -0,0 +1,433 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Image management routes."""
2
+ from flask import Blueprint, request, jsonify, current_app
3
+ from bson import ObjectId
4
+ from bson.errors import InvalidId
5
+ from datetime import datetime
6
+ from werkzeug.utils import secure_filename
7
+ import os
8
+
9
+ from app import mongo
10
+ from app.models import ImageModel, PersonModel
11
+ from app.services.cloudinary_service import upload_image, delete_image, get_thumbnail_url
12
+ from app.services.face_recognition_service import get_face_service
13
+
14
+ images_bp = Blueprint('images', __name__)
15
+
16
+
17
+ def allowed_file(filename):
18
+ """Check if file extension is allowed."""
19
+ allowed_extensions = current_app.config.get('ALLOWED_EXTENSIONS', {'png', 'jpg', 'jpeg', 'gif', 'webp'})
20
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions
21
+
22
+
23
+ @images_bp.route('', methods=['GET'])
24
+ def get_images():
25
+ """Get all images with filtering options."""
26
+ try:
27
+ page = int(request.args.get('page', 1))
28
+ per_page = int(request.args.get('per_page', 20))
29
+ filter_type = request.args.get('filter', 'all') # all, identified, unidentified
30
+
31
+ skip = (page - 1) * per_page
32
+
33
+ # Build query based on filter
34
+ query = {}
35
+ if filter_type == 'identified':
36
+ query['is_identified'] = True
37
+ elif filter_type == 'unidentified':
38
+ query['has_face'] = True
39
+ query['is_identified'] = False
40
+
41
+ # Get images with pagination
42
+ images = list(
43
+ mongo.db.images.find(query)
44
+ .sort('created_at', -1)
45
+ .skip(skip)
46
+ .limit(per_page)
47
+ )
48
+
49
+ # Get person info for identified images
50
+ person_ids = [img['person_id'] for img in images if img.get('person_id')]
51
+ people = {str(p['_id']): p for p in mongo.db.people.find({'_id': {'$in': person_ids}})}
52
+
53
+ # Attach person to images
54
+ for img in images:
55
+ if img.get('person_id') and str(img['person_id']) in people:
56
+ img['person'] = people[str(img['person_id'])]
57
+
58
+ total = mongo.db.images.count_documents(query)
59
+
60
+ return jsonify({
61
+ 'success': True,
62
+ 'data': [ImageModel.to_response(img, include_person=True) for img in images],
63
+ 'pagination': {
64
+ 'page': page,
65
+ 'per_page': per_page,
66
+ 'total': total,
67
+ 'pages': (total + per_page - 1) // per_page
68
+ }
69
+ })
70
+
71
+ except Exception as e:
72
+ return jsonify({'success': False, 'error': str(e)}), 500
73
+
74
+
75
+ @images_bp.route('/<image_id>', methods=['GET'])
76
+ def get_image(image_id):
77
+ """Get a specific image by ID."""
78
+ try:
79
+ image = mongo.db.images.find_one({'_id': ObjectId(image_id)})
80
+
81
+ if not image:
82
+ return jsonify({'success': False, 'error': 'Image not found'}), 404
83
+
84
+ # Get person info if assigned
85
+ if image.get('person_id'):
86
+ person = mongo.db.people.find_one({'_id': image['person_id']})
87
+ if person:
88
+ image['person'] = person
89
+
90
+ return jsonify({
91
+ 'success': True,
92
+ 'data': ImageModel.to_response(image, include_person=True)
93
+ })
94
+
95
+ except InvalidId:
96
+ return jsonify({'success': False, 'error': 'Invalid image ID'}), 400
97
+ except Exception as e:
98
+ return jsonify({'success': False, 'error': str(e)}), 500
99
+
100
+
101
+ @images_bp.route('/upload', methods=['POST'])
102
+ def upload_images():
103
+ """Upload one or more images with face detection."""
104
+ try:
105
+ if 'files' not in request.files:
106
+ return jsonify({'success': False, 'error': 'No files provided'}), 400
107
+
108
+ files = request.files.getlist('files')
109
+
110
+ if not files or all(f.filename == '' for f in files):
111
+ return jsonify({'success': False, 'error': 'No files selected'}), 400
112
+
113
+ face_service = get_face_service(current_app.config.get('FACE_RECOGNITION_TOLERANCE', 0.6))
114
+ people = list(mongo.db.people.find({'face_encodings': {'$ne': []}}))
115
+
116
+ results = []
117
+
118
+ for file in files:
119
+ if file and file.filename:
120
+ if not allowed_file(file.filename):
121
+ results.append({
122
+ 'filename': file.filename,
123
+ 'success': False,
124
+ 'error': 'File type not allowed'
125
+ })
126
+ continue
127
+
128
+ original_filename = secure_filename(file.filename)
129
+
130
+ # Read file data for both upload and face detection
131
+ file_data = file.read()
132
+
133
+ # Upload to Cloudinary
134
+ upload_result = upload_image(file_data)
135
+
136
+ if not upload_result['success']:
137
+ results.append({
138
+ 'filename': original_filename,
139
+ 'success': False,
140
+ 'error': upload_result.get('error', 'Upload failed')
141
+ })
142
+ continue
143
+
144
+ # Detect faces
145
+ face_result = face_service.detect_faces(file_data)
146
+
147
+ face_encodings = face_result.get('face_encodings', [])
148
+ face_locations = face_result.get('face_locations', [])
149
+
150
+ # Try to match faces to existing people
151
+ matched_person_id = None
152
+ if face_encodings:
153
+ for encoding in face_encodings:
154
+ matched_person_id = face_service.find_matching_person(encoding, people)
155
+ if matched_person_id:
156
+ break
157
+
158
+ # Create image document
159
+ image_doc = ImageModel.create_image(
160
+ cloudinary_url=upload_result['url'],
161
+ cloudinary_public_id=upload_result['public_id'],
162
+ original_filename=original_filename,
163
+ face_encodings=face_encodings,
164
+ face_locations=face_locations,
165
+ person_id=str(matched_person_id) if matched_person_id else None
166
+ )
167
+
168
+ result = mongo.db.images.insert_one(image_doc)
169
+ image_doc['_id'] = result.inserted_id
170
+
171
+ # Update person's image count and thumbnail if matched
172
+ if matched_person_id:
173
+ person = mongo.db.people.find_one({'_id': matched_person_id})
174
+ update_data = {
175
+ 'image_count': person.get('image_count', 0) + 1,
176
+ 'updated_at': datetime.utcnow()
177
+ }
178
+
179
+ # Set thumbnail if not set
180
+ if not person.get('thumbnail_url'):
181
+ update_data['thumbnail_url'] = get_thumbnail_url(upload_result['url'])
182
+
183
+ # Add face encoding to person if new
184
+ if face_encodings:
185
+ mongo.db.people.update_one(
186
+ {'_id': matched_person_id},
187
+ {
188
+ '$set': update_data,
189
+ '$addToSet': {'face_encodings': face_encodings[0]}
190
+ }
191
+ )
192
+ else:
193
+ mongo.db.people.update_one(
194
+ {'_id': matched_person_id},
195
+ {'$set': update_data}
196
+ )
197
+
198
+ results.append({
199
+ 'filename': original_filename,
200
+ 'success': True,
201
+ 'image': ImageModel.to_response(image_doc),
202
+ 'faces_detected': len(face_encodings),
203
+ 'matched_person': str(matched_person_id) if matched_person_id else None
204
+ })
205
+
206
+ successful = sum(1 for r in results if r['success'])
207
+
208
+ return jsonify({
209
+ 'success': True,
210
+ 'message': f'Uploaded {successful} of {len(results)} images',
211
+ 'results': results
212
+ })
213
+
214
+ except Exception as e:
215
+ return jsonify({'success': False, 'error': str(e)}), 500
216
+
217
+
218
+ @images_bp.route('/<image_id>', methods=['DELETE'])
219
+ def delete_image_route(image_id):
220
+ """Delete an image."""
221
+ try:
222
+ image = mongo.db.images.find_one({'_id': ObjectId(image_id)})
223
+
224
+ if not image:
225
+ return jsonify({'success': False, 'error': 'Image not found'}), 404
226
+
227
+ # Delete from Cloudinary
228
+ cloudinary_result = delete_image(image.get('cloudinary_public_id'))
229
+
230
+ # Update person's image count
231
+ if image.get('person_id'):
232
+ mongo.db.people.update_one(
233
+ {'_id': image['person_id']},
234
+ {
235
+ '$inc': {'image_count': -1},
236
+ '$set': {'updated_at': datetime.utcnow()}
237
+ }
238
+ )
239
+
240
+ # Delete from database
241
+ mongo.db.images.delete_one({'_id': ObjectId(image_id)})
242
+
243
+ return jsonify({
244
+ 'success': True,
245
+ 'message': 'Image deleted successfully'
246
+ })
247
+
248
+ except InvalidId:
249
+ return jsonify({'success': False, 'error': 'Invalid image ID'}), 400
250
+ except Exception as e:
251
+ return jsonify({'success': False, 'error': str(e)}), 500
252
+
253
+
254
+ @images_bp.route('/<image_id>/assign', methods=['PATCH'])
255
+ def assign_image(image_id):
256
+ """Assign or reassign an image to a person."""
257
+ try:
258
+ data = request.get_json()
259
+ person_id = data.get('person_id')
260
+
261
+ image = mongo.db.images.find_one({'_id': ObjectId(image_id)})
262
+ if not image:
263
+ return jsonify({'success': False, 'error': 'Image not found'}), 404
264
+
265
+ old_person_id = image.get('person_id')
266
+
267
+ # Handle unassignment
268
+ if not person_id:
269
+ mongo.db.images.update_one(
270
+ {'_id': ObjectId(image_id)},
271
+ {'$set': {
272
+ 'person_id': None,
273
+ 'is_identified': False,
274
+ 'updated_at': datetime.utcnow()
275
+ }}
276
+ )
277
+
278
+ # Update old person's count
279
+ if old_person_id:
280
+ mongo.db.people.update_one(
281
+ {'_id': old_person_id},
282
+ {
283
+ '$inc': {'image_count': -1},
284
+ '$set': {'updated_at': datetime.utcnow()}
285
+ }
286
+ )
287
+
288
+ return jsonify({
289
+ 'success': True,
290
+ 'message': 'Image unassigned successfully'
291
+ })
292
+
293
+ # Verify person exists
294
+ person = mongo.db.people.find_one({'_id': ObjectId(person_id)})
295
+ if not person:
296
+ return jsonify({'success': False, 'error': 'Person not found'}), 404
297
+
298
+ # Update image
299
+ mongo.db.images.update_one(
300
+ {'_id': ObjectId(image_id)},
301
+ {'$set': {
302
+ 'person_id': ObjectId(person_id),
303
+ 'is_identified': True,
304
+ 'updated_at': datetime.utcnow()
305
+ }}
306
+ )
307
+
308
+ # Update old person's count
309
+ if old_person_id and old_person_id != ObjectId(person_id):
310
+ mongo.db.people.update_one(
311
+ {'_id': old_person_id},
312
+ {
313
+ '$inc': {'image_count': -1},
314
+ '$set': {'updated_at': datetime.utcnow()}
315
+ }
316
+ )
317
+
318
+ # Update new person's count and face encoding
319
+ update_ops = {
320
+ '$set': {'updated_at': datetime.utcnow()}
321
+ }
322
+
323
+ if old_person_id != ObjectId(person_id):
324
+ update_ops['$inc'] = {'image_count': 1}
325
+
326
+ # Add face encoding to help future matching
327
+ if image.get('face_encodings') and len(image['face_encodings']) > 0:
328
+ mongo.db.people.update_one(
329
+ {'_id': ObjectId(person_id)},
330
+ {
331
+ **update_ops,
332
+ '$addToSet': {'face_encodings': image['face_encodings'][0]}
333
+ }
334
+ )
335
+ else:
336
+ mongo.db.people.update_one(
337
+ {'_id': ObjectId(person_id)},
338
+ update_ops
339
+ )
340
+
341
+ # Set thumbnail if not set
342
+ if not person.get('thumbnail_url'):
343
+ mongo.db.people.update_one(
344
+ {'_id': ObjectId(person_id)},
345
+ {'$set': {'thumbnail_url': get_thumbnail_url(image['cloudinary_url'])}}
346
+ )
347
+
348
+ return jsonify({
349
+ 'success': True,
350
+ 'message': f'Image assigned to {person["name"]}'
351
+ })
352
+
353
+ except InvalidId:
354
+ return jsonify({'success': False, 'error': 'Invalid ID'}), 400
355
+ except Exception as e:
356
+ return jsonify({'success': False, 'error': str(e)}), 500
357
+
358
+
359
+ @images_bp.route('/<image_id>/reprocess', methods=['POST'])
360
+ def reprocess_image(image_id):
361
+ """Reprocess an image to re-detect faces."""
362
+ try:
363
+ image = mongo.db.images.find_one({'_id': ObjectId(image_id)})
364
+
365
+ if not image:
366
+ return jsonify({'success': False, 'error': 'Image not found'}), 404
367
+
368
+ # Download image from Cloudinary and reprocess
369
+ import requests
370
+ from io import BytesIO
371
+
372
+ response = requests.get(image['cloudinary_url'])
373
+ if response.status_code != 200:
374
+ return jsonify({'success': False, 'error': 'Failed to download image'}), 500
375
+
376
+ face_service = get_face_service(current_app.config.get('FACE_RECOGNITION_TOLERANCE', 0.6))
377
+ face_result = face_service.detect_faces(response.content)
378
+
379
+ face_encodings = face_result.get('face_encodings', [])
380
+ face_locations = face_result.get('face_locations', [])
381
+
382
+ # Try to match faces
383
+ matched_person_id = None
384
+ if face_encodings:
385
+ people = list(mongo.db.people.find({'face_encodings': {'$ne': []}}))
386
+ for encoding in face_encodings:
387
+ matched_person_id = face_service.find_matching_person(encoding, people)
388
+ if matched_person_id:
389
+ break
390
+
391
+ # Update image
392
+ old_person_id = image.get('person_id')
393
+
394
+ update_data = {
395
+ 'face_encodings': face_encodings,
396
+ 'face_locations': face_locations,
397
+ 'has_face': bool(face_encodings),
398
+ 'updated_at': datetime.utcnow()
399
+ }
400
+
401
+ if matched_person_id:
402
+ update_data['person_id'] = matched_person_id
403
+ update_data['is_identified'] = True
404
+
405
+ mongo.db.images.update_one(
406
+ {'_id': ObjectId(image_id)},
407
+ {'$set': update_data}
408
+ )
409
+
410
+ # Update person counts
411
+ if old_person_id and old_person_id != matched_person_id:
412
+ mongo.db.people.update_one(
413
+ {'_id': old_person_id},
414
+ {'$inc': {'image_count': -1}}
415
+ )
416
+
417
+ if matched_person_id and old_person_id != matched_person_id:
418
+ mongo.db.people.update_one(
419
+ {'_id': matched_person_id},
420
+ {'$inc': {'image_count': 1}}
421
+ )
422
+
423
+ return jsonify({
424
+ 'success': True,
425
+ 'message': f'Reprocessed image. Found {len(face_encodings)} face(s).',
426
+ 'faces_detected': len(face_encodings),
427
+ 'matched_person': str(matched_person_id) if matched_person_id else None
428
+ })
429
+
430
+ except InvalidId:
431
+ return jsonify({'success': False, 'error': 'Invalid image ID'}), 400
432
+ except Exception as e:
433
+ return jsonify({'success': False, 'error': str(e)}), 500
app/routes/people.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """People management routes."""
2
+ from flask import Blueprint, request, jsonify
3
+ from bson import ObjectId
4
+ from bson.errors import InvalidId
5
+ from datetime import datetime
6
+
7
+ from app import mongo
8
+ from app.models import PersonModel, ImageModel
9
+ from app.services.cloudinary_service import get_thumbnail_url
10
+
11
+ people_bp = Blueprint('people', __name__)
12
+
13
+
14
+ @people_bp.route('', methods=['GET'])
15
+ def get_people():
16
+ """Get all people with optional search."""
17
+ try:
18
+ search = request.args.get('search', '').strip()
19
+ sort_by = request.args.get('sort', 'name')
20
+ order = request.args.get('order', 'asc')
21
+
22
+ # Build query
23
+ query = {}
24
+ if search:
25
+ query['name'] = {'$regex': search, '$options': 'i'}
26
+
27
+ # Sort options
28
+ sort_order = 1 if order == 'asc' else -1
29
+ sort_field = sort_by if sort_by in ['name', 'created_at', 'image_count'] else 'name'
30
+
31
+ people = list(mongo.db.people.find(query).sort(sort_field, sort_order))
32
+
33
+ return jsonify({
34
+ 'success': True,
35
+ 'data': [PersonModel.to_response(p) for p in people],
36
+ 'total': len(people)
37
+ })
38
+
39
+ except Exception as e:
40
+ return jsonify({'success': False, 'error': str(e)}), 500
41
+
42
+
43
+ @people_bp.route('/<person_id>', methods=['GET'])
44
+ def get_person(person_id):
45
+ """Get a specific person by ID."""
46
+ try:
47
+ person = mongo.db.people.find_one({'_id': ObjectId(person_id)})
48
+
49
+ if not person:
50
+ return jsonify({'success': False, 'error': 'Person not found'}), 404
51
+
52
+ return jsonify({
53
+ 'success': True,
54
+ 'data': PersonModel.to_response(person)
55
+ })
56
+
57
+ except InvalidId:
58
+ return jsonify({'success': False, 'error': 'Invalid person ID'}), 400
59
+ except Exception as e:
60
+ return jsonify({'success': False, 'error': str(e)}), 500
61
+
62
+
63
+ @people_bp.route('', methods=['POST'])
64
+ def create_person():
65
+ """Create a new person."""
66
+ try:
67
+ data = request.get_json()
68
+
69
+ if not data or not data.get('name'):
70
+ return jsonify({'success': False, 'error': 'Name is required'}), 400
71
+
72
+ name = data['name'].strip()
73
+
74
+ # Check if person with same name exists
75
+ existing = mongo.db.people.find_one({'name': {'$regex': f'^{name}$', '$options': 'i'}})
76
+ if existing:
77
+ return jsonify({'success': False, 'error': 'A person with this name already exists'}), 400
78
+
79
+ person = PersonModel.create_person(name)
80
+ result = mongo.db.people.insert_one(person)
81
+
82
+ person['_id'] = result.inserted_id
83
+
84
+ return jsonify({
85
+ 'success': True,
86
+ 'message': 'Person created successfully',
87
+ 'data': PersonModel.to_response(person)
88
+ }), 201
89
+
90
+ except Exception as e:
91
+ return jsonify({'success': False, 'error': str(e)}), 500
92
+
93
+
94
+ @people_bp.route('/<person_id>', methods=['PUT'])
95
+ def update_person(person_id):
96
+ """Update a person's details."""
97
+ try:
98
+ data = request.get_json()
99
+
100
+ if not data:
101
+ return jsonify({'success': False, 'error': 'No data provided'}), 400
102
+
103
+ person = mongo.db.people.find_one({'_id': ObjectId(person_id)})
104
+ if not person:
105
+ return jsonify({'success': False, 'error': 'Person not found'}), 404
106
+
107
+ update_data = {'updated_at': datetime.utcnow()}
108
+
109
+ if 'name' in data:
110
+ name = data['name'].strip()
111
+ if not name:
112
+ return jsonify({'success': False, 'error': 'Name cannot be empty'}), 400
113
+
114
+ # Check for duplicate name
115
+ existing = mongo.db.people.find_one({
116
+ 'name': {'$regex': f'^{name}$', '$options': 'i'},
117
+ '_id': {'$ne': ObjectId(person_id)}
118
+ })
119
+ if existing:
120
+ return jsonify({'success': False, 'error': 'A person with this name already exists'}), 400
121
+
122
+ update_data['name'] = name
123
+
124
+ mongo.db.people.update_one(
125
+ {'_id': ObjectId(person_id)},
126
+ {'$set': update_data}
127
+ )
128
+
129
+ updated_person = mongo.db.people.find_one({'_id': ObjectId(person_id)})
130
+
131
+ return jsonify({
132
+ 'success': True,
133
+ 'message': 'Person updated successfully',
134
+ 'data': PersonModel.to_response(updated_person)
135
+ })
136
+
137
+ except InvalidId:
138
+ return jsonify({'success': False, 'error': 'Invalid person ID'}), 400
139
+ except Exception as e:
140
+ return jsonify({'success': False, 'error': str(e)}), 500
141
+
142
+
143
+ @people_bp.route('/<person_id>', methods=['DELETE'])
144
+ def delete_person(person_id):
145
+ """Delete a person and optionally their images."""
146
+ try:
147
+ delete_images = request.args.get('delete_images', 'false').lower() == 'true'
148
+
149
+ person = mongo.db.people.find_one({'_id': ObjectId(person_id)})
150
+ if not person:
151
+ return jsonify({'success': False, 'error': 'Person not found'}), 404
152
+
153
+ if delete_images:
154
+ # Delete all images associated with this person
155
+ from app.services.cloudinary_service import delete_image
156
+
157
+ images = mongo.db.images.find({'person_id': ObjectId(person_id)})
158
+ for image in images:
159
+ delete_image(image.get('cloudinary_public_id'))
160
+
161
+ mongo.db.images.delete_many({'person_id': ObjectId(person_id)})
162
+ else:
163
+ # Just unassign images from this person
164
+ mongo.db.images.update_many(
165
+ {'person_id': ObjectId(person_id)},
166
+ {'$set': {'person_id': None, 'is_identified': False, 'updated_at': datetime.utcnow()}}
167
+ )
168
+
169
+ # Delete the person
170
+ mongo.db.people.delete_one({'_id': ObjectId(person_id)})
171
+
172
+ return jsonify({
173
+ 'success': True,
174
+ 'message': f'Person deleted successfully. Images {"deleted" if delete_images else "unassigned"}.'
175
+ })
176
+
177
+ except InvalidId:
178
+ return jsonify({'success': False, 'error': 'Invalid person ID'}), 400
179
+ except Exception as e:
180
+ return jsonify({'success': False, 'error': str(e)}), 500
181
+
182
+
183
+ @people_bp.route('/<person_id>/images', methods=['GET'])
184
+ def get_person_images(person_id):
185
+ """Get all images for a specific person."""
186
+ try:
187
+ person = mongo.db.people.find_one({'_id': ObjectId(person_id)})
188
+ if not person:
189
+ return jsonify({'success': False, 'error': 'Person not found'}), 404
190
+
191
+ page = int(request.args.get('page', 1))
192
+ per_page = int(request.args.get('per_page', 20))
193
+ skip = (page - 1) * per_page
194
+
195
+ images = list(
196
+ mongo.db.images.find({'person_id': ObjectId(person_id)})
197
+ .sort('created_at', -1)
198
+ .skip(skip)
199
+ .limit(per_page)
200
+ )
201
+
202
+ total = mongo.db.images.count_documents({'person_id': ObjectId(person_id)})
203
+
204
+ return jsonify({
205
+ 'success': True,
206
+ 'data': [ImageModel.to_response(img) for img in images],
207
+ 'person': PersonModel.to_response(person),
208
+ 'pagination': {
209
+ 'page': page,
210
+ 'per_page': per_page,
211
+ 'total': total,
212
+ 'pages': (total + per_page - 1) // per_page
213
+ }
214
+ })
215
+
216
+ except InvalidId:
217
+ return jsonify({'success': False, 'error': 'Invalid person ID'}), 400
218
+ except Exception as e:
219
+ return jsonify({'success': False, 'error': str(e)}), 500
app/routes/stats.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Statistics and dashboard routes."""
2
+ from flask import Blueprint, jsonify
3
+ from datetime import datetime, timedelta
4
+
5
+ from app import mongo
6
+ from app.models import PersonModel, ImageModel
7
+
8
+ stats_bp = Blueprint('stats', __name__)
9
+
10
+
11
+ @stats_bp.route('', methods=['GET'])
12
+ def get_stats():
13
+ """Get dashboard statistics."""
14
+ try:
15
+ # Count totals
16
+ total_images = mongo.db.images.count_documents({})
17
+ total_people = mongo.db.people.count_documents({})
18
+ identified_images = mongo.db.images.count_documents({'is_identified': True})
19
+ unidentified_faces = mongo.db.images.count_documents({
20
+ 'has_face': True,
21
+ 'is_identified': False
22
+ })
23
+
24
+ # Calculate percentages
25
+ identification_rate = (identified_images / total_images * 100) if total_images > 0 else 0
26
+
27
+ return jsonify({
28
+ 'success': True,
29
+ 'data': {
30
+ 'total_images': total_images,
31
+ 'total_people': total_people,
32
+ 'identified_images': identified_images,
33
+ 'unidentified_faces': unidentified_faces,
34
+ 'identification_rate': round(identification_rate, 1)
35
+ }
36
+ })
37
+
38
+ except Exception as e:
39
+ return jsonify({'success': False, 'error': str(e)}), 500
40
+
41
+
42
+ @stats_bp.route('/recent', methods=['GET'])
43
+ def get_recent():
44
+ """Get recent uploads."""
45
+ try:
46
+ # Get recent images (last 7 days)
47
+ week_ago = datetime.utcnow() - timedelta(days=7)
48
+
49
+ recent_images = list(
50
+ mongo.db.images.find({'created_at': {'$gte': week_ago}})
51
+ .sort('created_at', -1)
52
+ .limit(12)
53
+ )
54
+
55
+ # Get person info for identified images
56
+ person_ids = [img['person_id'] for img in recent_images if img.get('person_id')]
57
+ people = {str(p['_id']): p for p in mongo.db.people.find({'_id': {'$in': person_ids}})}
58
+
59
+ for img in recent_images:
60
+ if img.get('person_id') and str(img['person_id']) in people:
61
+ img['person'] = people[str(img['person_id'])]
62
+
63
+ return jsonify({
64
+ 'success': True,
65
+ 'data': [ImageModel.to_response(img, include_person=True) for img in recent_images]
66
+ })
67
+
68
+ except Exception as e:
69
+ return jsonify({'success': False, 'error': str(e)}), 500
70
+
71
+
72
+ @stats_bp.route('/unidentified', methods=['GET'])
73
+ def get_unidentified():
74
+ """Get images with unidentified faces."""
75
+ try:
76
+ unidentified = list(
77
+ mongo.db.images.find({
78
+ 'has_face': True,
79
+ 'is_identified': False
80
+ })
81
+ .sort('created_at', -1)
82
+ .limit(12)
83
+ )
84
+
85
+ return jsonify({
86
+ 'success': True,
87
+ 'data': [ImageModel.to_response(img) for img in unidentified]
88
+ })
89
+
90
+ except Exception as e:
91
+ return jsonify({'success': False, 'error': str(e)}), 500
92
+
93
+
94
+ @stats_bp.route('/people-summary', methods=['GET'])
95
+ def get_people_summary():
96
+ """Get people with most images."""
97
+ try:
98
+ people = list(
99
+ mongo.db.people.find()
100
+ .sort('image_count', -1)
101
+ .limit(8)
102
+ )
103
+
104
+ return jsonify({
105
+ 'success': True,
106
+ 'data': [PersonModel.to_response(p) for p in people]
107
+ })
108
+
109
+ except Exception as e:
110
+ return jsonify({'success': False, 'error': str(e)}), 500
app/services/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Services package."""
2
+ from .cloudinary_service import upload_image, delete_image, get_thumbnail_url, init_cloudinary
3
+ from .face_recognition_service import FaceRecognitionService, get_face_service
4
+
5
+ __all__ = [
6
+ 'upload_image',
7
+ 'delete_image',
8
+ 'get_thumbnail_url',
9
+ 'init_cloudinary',
10
+ 'FaceRecognitionService',
11
+ 'get_face_service'
12
+ ]
app/services/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (475 Bytes). View file
 
app/services/__pycache__/cloudinary_service.cpython-310.pyc ADDED
Binary file (2.44 kB). View file
 
app/services/__pycache__/face_recognition_service.cpython-310.pyc ADDED
Binary file (4.12 kB). View file
 
app/services/cloudinary_service.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Cloudinary service for image uploads and management."""
2
+ import cloudinary
3
+ import cloudinary.uploader
4
+ import cloudinary.api
5
+ from werkzeug.utils import secure_filename
6
+
7
+
8
+ def init_cloudinary(config):
9
+ """Initialize Cloudinary with configuration."""
10
+ cloudinary.config(
11
+ cloud_name=config.get('CLOUDINARY_CLOUD_NAME'),
12
+ api_key=config.get('CLOUDINARY_API_KEY'),
13
+ api_secret=config.get('CLOUDINARY_API_SECRET'),
14
+ secure=True
15
+ )
16
+
17
+
18
+ def upload_image(file, folder='image-organizer'):
19
+ """
20
+ Upload an image to Cloudinary.
21
+
22
+ Args:
23
+ file: File object or file path
24
+ folder: Cloudinary folder to store the image
25
+
26
+ Returns:
27
+ dict: Upload result containing url and public_id
28
+ """
29
+ try:
30
+ result = cloudinary.uploader.upload(
31
+ file,
32
+ folder=folder,
33
+ resource_type='image',
34
+ transformation=[
35
+ {'quality': 'auto:good'},
36
+ {'fetch_format': 'auto'}
37
+ ]
38
+ )
39
+ return {
40
+ 'success': True,
41
+ 'url': result['secure_url'],
42
+ 'public_id': result['public_id'],
43
+ 'width': result.get('width'),
44
+ 'height': result.get('height'),
45
+ 'format': result.get('format')
46
+ }
47
+ except Exception as e:
48
+ return {
49
+ 'success': False,
50
+ 'error': str(e)
51
+ }
52
+
53
+
54
+ def delete_image(public_id):
55
+ """
56
+ Delete an image from Cloudinary.
57
+
58
+ Args:
59
+ public_id: Cloudinary public ID of the image
60
+
61
+ Returns:
62
+ dict: Deletion result
63
+ """
64
+ try:
65
+ result = cloudinary.uploader.destroy(public_id)
66
+ return {
67
+ 'success': result.get('result') == 'ok',
68
+ 'result': result
69
+ }
70
+ except Exception as e:
71
+ return {
72
+ 'success': False,
73
+ 'error': str(e)
74
+ }
75
+
76
+
77
+ def get_thumbnail_url(url, width=200, height=200):
78
+ """
79
+ Generate a thumbnail URL for an image.
80
+
81
+ Args:
82
+ url: Original Cloudinary URL
83
+ width: Thumbnail width
84
+ height: Thumbnail height
85
+
86
+ Returns:
87
+ str: Thumbnail URL
88
+ """
89
+ if not url:
90
+ return None
91
+
92
+ # Insert transformation into Cloudinary URL
93
+ parts = url.split('/upload/')
94
+ if len(parts) == 2:
95
+ transformation = f'c_fill,w_{width},h_{height},g_face'
96
+ return f'{parts[0]}/upload/{transformation}/{parts[1]}'
97
+ return url
app/services/face_recognition_service.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Face recognition service for detecting and matching faces."""
2
+ import face_recognition
3
+ import numpy as np
4
+ from io import BytesIO
5
+ from PIL import Image
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class FaceRecognitionService:
12
+ """Service for face detection and recognition."""
13
+
14
+ def __init__(self, tolerance=0.6):
15
+ """
16
+ Initialize the face recognition service.
17
+
18
+ Args:
19
+ tolerance: How much distance between faces to consider it a match.
20
+ Lower is stricter. 0.6 is typical best performance.
21
+ """
22
+ self.tolerance = tolerance
23
+
24
+ def detect_faces(self, image_data):
25
+ """
26
+ Detect faces in an image.
27
+
28
+ Args:
29
+ image_data: Image file data (bytes or file-like object)
30
+
31
+ Returns:
32
+ dict: Contains face_encodings (list) and face_locations (list)
33
+ """
34
+ try:
35
+ # Load image
36
+ if isinstance(image_data, bytes):
37
+ image = face_recognition.load_image_file(BytesIO(image_data))
38
+ else:
39
+ image_data.seek(0)
40
+ image = face_recognition.load_image_file(image_data)
41
+
42
+ # Detect face locations
43
+ face_locations = face_recognition.face_locations(image, model='hog')
44
+
45
+ if not face_locations:
46
+ return {
47
+ 'success': True,
48
+ 'face_encodings': [],
49
+ 'face_locations': [],
50
+ 'face_count': 0
51
+ }
52
+
53
+ # Get face encodings
54
+ face_encodings = face_recognition.face_encodings(image, face_locations)
55
+
56
+ # Convert to serializable format
57
+ encodings_list = [encoding.tolist() for encoding in face_encodings]
58
+ locations_list = [list(loc) for loc in face_locations]
59
+
60
+ return {
61
+ 'success': True,
62
+ 'face_encodings': encodings_list,
63
+ 'face_locations': locations_list,
64
+ 'face_count': len(face_locations)
65
+ }
66
+
67
+ except Exception as e:
68
+ logger.error(f"Face detection error: {str(e)}")
69
+ return {
70
+ 'success': False,
71
+ 'error': str(e),
72
+ 'face_encodings': [],
73
+ 'face_locations': [],
74
+ 'face_count': 0
75
+ }
76
+
77
+ def find_matching_person(self, face_encoding, people):
78
+ """
79
+ Find a matching person for a face encoding.
80
+
81
+ Args:
82
+ face_encoding: The face encoding to match (list)
83
+ people: List of person documents with face_encodings
84
+
85
+ Returns:
86
+ ObjectId or None: The matched person's ID or None
87
+ """
88
+ if not face_encoding or not people:
89
+ return None
90
+
91
+ target_encoding = np.array(face_encoding)
92
+ best_match = None
93
+ best_distance = float('inf')
94
+
95
+ for person in people:
96
+ person_encodings = person.get('face_encodings', [])
97
+
98
+ for stored_encoding in person_encodings:
99
+ if not stored_encoding:
100
+ continue
101
+
102
+ stored_np = np.array(stored_encoding)
103
+
104
+ # Calculate face distance
105
+ distance = np.linalg.norm(target_encoding - stored_np)
106
+
107
+ if distance < self.tolerance and distance < best_distance:
108
+ best_distance = distance
109
+ best_match = person['_id']
110
+
111
+ return best_match
112
+
113
+ def compare_faces(self, encoding1, encoding2):
114
+ """
115
+ Compare two face encodings.
116
+
117
+ Args:
118
+ encoding1: First face encoding (list)
119
+ encoding2: Second face encoding (list)
120
+
121
+ Returns:
122
+ dict: Contains 'match' (bool) and 'distance' (float)
123
+ """
124
+ try:
125
+ enc1 = np.array(encoding1)
126
+ enc2 = np.array(encoding2)
127
+
128
+ distance = np.linalg.norm(enc1 - enc2)
129
+ is_match = distance <= self.tolerance
130
+
131
+ return {
132
+ 'match': is_match,
133
+ 'distance': float(distance),
134
+ 'confidence': max(0, 1 - (distance / self.tolerance)) if is_match else 0
135
+ }
136
+ except Exception as e:
137
+ logger.error(f"Face comparison error: {str(e)}")
138
+ return {
139
+ 'match': False,
140
+ 'distance': float('inf'),
141
+ 'confidence': 0,
142
+ 'error': str(e)
143
+ }
144
+
145
+
146
+ # Global service instance
147
+ face_service = None
148
+
149
+
150
+ def get_face_service(tolerance=0.6):
151
+ """Get or create the face recognition service instance."""
152
+ global face_service
153
+ if face_service is None:
154
+ face_service = FaceRecognitionService(tolerance)
155
+ return face_service
config.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Application configuration."""
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+
8
+ class Config:
9
+ """Base configuration."""
10
+ SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key')
11
+ MONGO_URI = os.environ.get('MONGODB_URI')
12
+
13
+ # Cloudinary settings
14
+ CLOUDINARY_CLOUD_NAME = os.environ.get('CLOUDINARY_CLOUD_NAME')
15
+ CLOUDINARY_API_KEY = os.environ.get('CLOUDINARY_API_KEY')
16
+ CLOUDINARY_API_SECRET = os.environ.get('CLOUDINARY_API_SECRET')
17
+
18
+ # Face recognition settings
19
+ FACE_RECOGNITION_TOLERANCE = float(os.environ.get('FACE_RECOGNITION_TOLERANCE', 0.6))
20
+
21
+ # File upload settings
22
+ MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB
23
+ ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
24
+
25
+
26
+ class DevelopmentConfig(Config):
27
+ """Development configuration."""
28
+ DEBUG = True
29
+
30
+
31
+ class ProductionConfig(Config):
32
+ """Production configuration."""
33
+ DEBUG = False
34
+
35
+
36
+ class TestingConfig(Config):
37
+ """Testing configuration."""
38
+ TESTING = True
39
+
40
+
41
+ config = {
42
+ 'development': DevelopmentConfig,
43
+ 'production': ProductionConfig,
44
+ 'testing': TestingConfig,
45
+ 'default': DevelopmentConfig
46
+ }
requirements.txt ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Flask and extensions
2
+ Flask==3.0.0
3
+ Flask-CORS==4.0.0
4
+ Flask-PyMongo==2.3.0
5
+ python-dotenv==1.0.0
6
+
7
+ # MongoDB
8
+ pymongo==4.6.1
9
+ dnspython==2.4.2
10
+
11
+ # Face Recognition
12
+ face-recognition==1.3.0
13
+ numpy==1.26.2
14
+ Pillow==10.1.0
15
+
16
+ # Cloudinary
17
+ cloudinary==1.37.0
18
+
19
+ # Utilities
20
+ Werkzeug==3.0.1
21
+ gunicorn==21.2.0
22
+ python-dateutil==2.8.2
23
+ requests==2.32.5
run.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """Application entry point."""
2
+ from app import create_app
3
+
4
+ app = create_app()
5
+
6
+ if __name__ == '__main__':
7
+ app.run(host='0.0.0.0', port=5000, debug=True)