-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
344 lines (303 loc) · 11.9 KB
/
app.py
File metadata and controls
344 lines (303 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
#!/usr/bin/env python3
"""
Production OMR Scraper Application
Optimized for Digital Ocean deployment
"""
import os
import sys
import tempfile
import json
import logging
from flask import Flask, request, jsonify, send_file
from werkzeug.utils import secure_filename
from omr_analyzer import OMRAnalyzer
from scheduler import start_cleanup_scheduler, stop_cleanup_scheduler, manual_cleanup, get_scheduler_status
from production_config import ProductionConfig
# Configure logging
logging.basicConfig(
level=getattr(logging, ProductionConfig.LOG_LEVEL),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(ProductionConfig.LOG_FILE),
logging.StreamHandler(sys.stdout)
]
)
app = Flask(__name__)
app.config.from_object(ProductionConfig)
# Configure upload settings
ALLOWED_EXTENSIONS = ProductionConfig.ALLOWED_EXTENSIONS
def allowed_file(filename):
"""Check if uploaded file has allowed extension"""
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
def convert_to_bangla_options(answers):
"""Convert numeric answers to Bengali letters"""
conversion_map = {
1: 'ক', # ka
2: 'খ', # kha
3: 'গ', # ga
4: 'ঘ' # gha
}
bangla_answers = {}
for question, answer in answers.items():
if isinstance(answer, (int, str)):
try:
numeric_answer = int(answer)
if numeric_answer in conversion_map:
bangla_answers[question] = conversion_map[numeric_answer]
else:
bangla_answers[question] = answer # Keep original if not in range 1-4
except (ValueError, TypeError):
bangla_answers[question] = answer # Keep original if not numeric
else:
bangla_answers[question] = answer
return bangla_answers
def compare_answers(detected_answers, correct_answers):
"""Compare detected answers with correct answers and return match results"""
if not correct_answers:
return {}, {}, {}
matched = {}
unmatched = {}
missing = {}
# Convert correct_answers keys to integers if they're strings
if isinstance(correct_answers, dict):
correct_answers_normalized = {}
for key, value in correct_answers.items():
try:
correct_answers_normalized[int(key)] = value
except (ValueError, TypeError):
correct_answers_normalized[key] = value
correct_answers = correct_answers_normalized
# Compare each detected answer with correct answer
for question, detected in detected_answers.items():
if question in correct_answers:
if detected == correct_answers[question]:
matched[question] = detected
else:
unmatched[question] = {
'detected': detected,
'correct': correct_answers[question]
}
else:
# No correct answer provided for this question
matched[question] = detected
# Find missing answers (questions with correct answers but no detection)
for question, correct in correct_answers.items():
if question not in detected_answers:
missing[question] = correct
return matched, unmatched, missing
@app.route('/analyze-omr', methods=['POST'])
def analyze_omr():
"""API endpoint to analyze OMR sheet from uploaded image"""
try:
# Check if file is present in request
if 'image' not in request.files:
return jsonify({
'success': False,
'error': 'No image file provided. Please upload an image with key "image"'
}), 400
file = request.files['image']
# Check if file is selected
if file.filename == '':
return jsonify({
'success': False,
'error': 'No file selected'
}), 400
# Check file extension
if not allowed_file(file.filename):
return jsonify({
'success': False,
'error': f'Invalid file type. Allowed types: {", ".join(ALLOWED_EXTENSIONS)}'
}), 400
# Save uploaded file to temporary location
filename = secure_filename(file.filename)
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(filename)[1]) as temp_file:
file.save(temp_file.name)
temp_path = temp_file.name
marked_image_url = None
try:
# Parse correct answers if provided
correct_answers = None
if 'correct_answers' in request.form:
try:
correct_answers = json.loads(request.form.get('correct_answers'))
except (json.JSONDecodeError, TypeError):
return jsonify({
'success': False,
'error': 'Invalid JSON format for correct_answers'
}), 400
# Create analyzer and process the image
analyzer = OMRAnalyzer()
raw_answers = analyzer.analyze_omr(temp_path)
# Convert numeric answers to Bengali letters
bangla_answers = convert_to_bangla_options(raw_answers)
# Compare with correct answers if provided
matched = {}
unmatched = {}
missing = {}
comparison_results = None
if correct_answers:
matched, unmatched, missing = compare_answers(bangla_answers, correct_answers)
comparison_results = {
'total_correct': len(matched),
'total_incorrect': len(unmatched),
'total_missing': len(missing),
'accuracy_percentage': round((len(matched) / len(correct_answers)) * 100, 2) if correct_answers else 0,
'matched_answers': matched,
'unmatched_answers': unmatched,
'missing_answers': missing
}
# Generate marked image with correct answers highlighted
marked_image_path = None
marked_image_url = None
# Always generate marked image when correct answers are provided
marked_image_path = analyzer.mark_unmatched_answers(temp_path, unmatched, correct_answers)
if marked_image_path:
# Generate download URL
marked_filename = os.path.basename(marked_image_path)
marked_image_url = f"/download-marked-image/{marked_filename}"
# Prepare JSON response
response_data = {
'success': True,
'filename': filename,
'total_questions': len(bangla_answers),
'answers': bangla_answers,
'comparison': comparison_results,
'marked_image_url': marked_image_url,
'student_answers': correct_answers
}
return jsonify(response_data), 200
finally:
# Clean up temporary file but keep marked image
if os.path.exists(temp_path):
os.unlink(temp_path)
# Note: marked_image_path is kept for user to download
except Exception as e:
logging.error(f"Error processing image: {str(e)}")
return jsonify({
'success': False,
'error': f'Error processing image: {str(e)}'
}), 500
@app.route('/download-marked-image/<path:filename>')
def download_marked_image(filename):
"""Download marked image with correct answers highlighted"""
try:
# Construct the full path
marked_dir = os.path.join(os.getcwd(), 'marked_images')
file_path = os.path.join(marked_dir, filename)
if os.path.exists(file_path):
return send_file(file_path, as_attachment=True)
else:
return jsonify({
'success': False,
'error': 'File not found'
}), 404
except Exception as e:
logging.error(f"Error downloading file: {str(e)}")
return jsonify({
'success': False,
'error': f'Error downloading file: {str(e)}'
}), 500
@app.route('/cleanup/manual', methods=['POST'])
def manual_cleanup_endpoint():
"""Manually trigger cleanup of marked_images folder"""
try:
manual_cleanup()
return jsonify({
'success': True,
'message': 'Manual cleanup completed successfully'
}), 200
except Exception as e:
logging.error(f"Error during manual cleanup: {str(e)}")
return jsonify({
'success': False,
'error': f'Error during manual cleanup: {str(e)}'
}), 500
@app.route('/cleanup/status', methods=['GET'])
def cleanup_status():
"""Get current status of cleanup scheduler"""
try:
status = get_scheduler_status()
return jsonify({
'success': True,
'scheduler_status': status
}), 200
except Exception as e:
logging.error(f"Error getting scheduler status: {str(e)}")
return jsonify({
'success': False,
'error': f'Error getting scheduler status: {str(e)}'
}), 500
@app.route('/cleanup/start', methods=['POST'])
def start_scheduler_endpoint():
"""Start the cleanup scheduler"""
try:
start_cleanup_scheduler()
return jsonify({
'success': True,
'message': 'Cleanup scheduler started successfully'
}), 200
except Exception as e:
logging.error(f"Error starting scheduler: {str(e)}")
return jsonify({
'success': False,
'error': f'Error starting scheduler: {str(e)}'
}), 500
@app.route('/cleanup/stop', methods=['POST'])
def stop_scheduler_endpoint():
"""Stop the cleanup scheduler"""
try:
stop_cleanup_scheduler()
return jsonify({
'success': True,
'message': 'Cleanup scheduler stopped successfully'
}), 200
except Exception as e:
logging.error(f"Error stopping scheduler: {str(e)}")
return jsonify({
'success': False,
'error': f'Error stopping scheduler: {str(e)}'
}), 500
@app.route('/', methods=['GET'])
def home():
"""Home endpoint with API information"""
return jsonify({
'message': 'OMR Analyzer API with Background Cleanup Scheduler',
'version': '1.0.0',
'environment': 'production',
'supported_formats': list(ALLOWED_EXTENSIONS),
'endpoints': {
'analyze': '/analyze-omr',
'download': '/download-marked-image/<filename>',
'cleanup_manual': '/cleanup/manual',
'cleanup_status': '/cleanup/status',
'cleanup_start': '/cleanup/start',
'cleanup_stop': '/cleanup/stop'
}
}), 200
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint for load balancer"""
return jsonify({
'status': 'healthy',
'service': 'omr-scraper',
'version': '1.0.0'
}), 200
if __name__ == "__main__":
# Create necessary directories
os.makedirs(ProductionConfig.UPLOAD_FOLDER, exist_ok=True)
os.makedirs('marked_images', exist_ok=True)
# Start the background cleanup scheduler
logging.info("Starting background cleanup scheduler...")
start_cleanup_scheduler()
logging.info("Background cleanup scheduler started successfully!")
try:
app.run(
debug=ProductionConfig.DEBUG,
host=ProductionConfig.HOST,
port=ProductionConfig.PORT
)
except KeyboardInterrupt:
logging.info("Shutting down...")
stop_cleanup_scheduler()
logging.info("Cleanup scheduler stopped.")