devranx commited on
Commit
03e275e
·
verified ·
1 Parent(s): fd41dfc

Upload 20 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Install system dependencies for OpenCV and others
4
+ RUN apt-get update && apt-get install -y \
5
+ libgl1-mesa-glx \
6
+ libglib2.0-0 \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ # Set working directory
10
+ WORKDIR /app
11
+
12
+ # Copy requirements first to leverage Docker cache
13
+ COPY requirements.txt .
14
+
15
+ # Install Python dependencies
16
+ # Use --no-cache-dir to keep image size down
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy the rest of the application
20
+ COPY . .
21
+
22
+ # Create directories for uploads and reports if they don't exist
23
+ RUN mkdir -p static/uploads/single static/uploads/multiple Reports Models
24
+
25
+ # Expose the port Hugging Face Spaces expects
26
+ EXPOSE 7860
27
+
28
+ # Command to run the application
29
+ CMD ["python", "app.py"]
README.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Prism Content Moderation
3
+ emoji: 🛡️
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ license: mit
10
+ ---
11
+
12
+ # Prism: AI-Powered Content Moderation
13
+
14
+ Prism is an automated content moderation pipeline that uses advanced AI models to audit images for compliance, safety, and quality.
15
+
16
+ ## Features
17
+ - **Text Analysis:** OCR (EasyOCR) + VLM (InternVL) to check taglines, CTAs, and Terms & Conditions.
18
+ - **Visual Checks:** Detects ribbons, gestures, and assesses image quality.
19
+ - **Content Safety:** Flags offensive, risky (gambling/trading), or illegal content using CLIP and text analysis.
20
+
21
+ ## Models Used
22
+ - **InternVL2_5-1B-MPO:** Multimodal LLM for detailed image understanding.
23
+ - **EasyOCR:** For robust text extraction.
24
+ - **CLIP (ViT-B/32):** For zero-shot image classification (theme detection).
25
+
26
+ ## Usage
27
+ Upload an image to get a detailed compliance report.
app.py ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify, send_file, Response, stream_with_context
2
+ from werkzeug.utils import secure_filename
3
+ import os
4
+ from pathlib import Path
5
+ import shutil
6
+ import io
7
+ import zipfile
8
+ import base64
9
+ import json
10
+ from pipeline import classify # Ensure this is your classification logic
11
+
12
+ app = Flask(__name__)
13
+
14
+ # Configuration
15
+ UPLOAD_FOLDER_SINGLE = 'static/uploads/single'
16
+ UPLOAD_FOLDER_MULTIPLE = 'static/uploads/multiple'
17
+ ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
18
+
19
+ app.config['UPLOAD_FOLDER_SINGLE'] = UPLOAD_FOLDER_SINGLE
20
+ app.config['UPLOAD_FOLDER_MULTIPLE'] = UPLOAD_FOLDER_MULTIPLE
21
+ app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # Increase to 100MB
22
+ app.config['MAX_CONTENT_LENGTH_REPORT'] = 500 * 1024 * 1024 # 500MB for reports
23
+
24
+ # Ensure upload directories exist
25
+ os.makedirs(UPLOAD_FOLDER_SINGLE, exist_ok=True)
26
+ os.makedirs(UPLOAD_FOLDER_MULTIPLE, exist_ok=True)
27
+
28
+ def allowed_file(filename):
29
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
30
+
31
+ def clear_uploads(folder):
32
+ upload_dir = Path(folder)
33
+ if upload_dir.exists():
34
+ shutil.rmtree(upload_dir)
35
+ upload_dir.mkdir(parents=True)
36
+
37
+ # Routes
38
+ @app.route('/')
39
+ def index():
40
+ return render_template('index.html')
41
+
42
+ @app.route('/single')
43
+ def single():
44
+ return render_template('single.html')
45
+
46
+ @app.route('/multiple')
47
+ def multiple():
48
+ return render_template('multiple.html')
49
+
50
+ @app.route('/clear_uploads', methods=['POST'])
51
+ def clear_uploads_route():
52
+ try:
53
+ clear_uploads(app.config['UPLOAD_FOLDER_SINGLE'])
54
+ clear_uploads(app.config['UPLOAD_FOLDER_MULTIPLE'])
55
+ return jsonify({'message': 'Upload folders cleared successfully'})
56
+ except Exception as e:
57
+ return jsonify({'error': str(e)}), 500
58
+
59
+ # Single Image Classification Routes
60
+ @app.route('/upload_single', methods=['POST'])
61
+ def upload_single():
62
+ if 'file' not in request.files:
63
+ return jsonify({'error': 'No file part'}), 400
64
+
65
+ file = request.files['file']
66
+ if file.filename == '':
67
+ return jsonify({'error': 'No selected file'}), 400
68
+
69
+ if file and allowed_file(file.filename):
70
+ # Clear old files
71
+ clear_uploads(app.config['UPLOAD_FOLDER_SINGLE'])
72
+
73
+ # Save new file
74
+ filename = secure_filename(file.filename)
75
+ filepath = os.path.join(app.config['UPLOAD_FOLDER_SINGLE'], filename)
76
+ file.save(filepath)
77
+
78
+ return jsonify({
79
+ 'message': 'File uploaded successfully',
80
+ 'filename': filename
81
+ })
82
+
83
+ return jsonify({'error': 'Invalid file type'}), 400
84
+
85
+ @app.route('/classify_single', methods=['POST'])
86
+ def classify_single():
87
+ data = request.get_json()
88
+ filename = data.get('filename')
89
+ if not filename:
90
+ return jsonify({'error': 'No filename provided'}), 400
91
+
92
+ filepath = os.path.join(app.config['UPLOAD_FOLDER_SINGLE'], filename)
93
+ if not os.path.exists(filepath):
94
+ return jsonify({'error': 'File not found'}), 404
95
+
96
+ try:
97
+ classification_result, result_table, failure_labels = classify(filepath)
98
+ return jsonify({
99
+ 'classification': classification_result,
100
+ 'result_table': result_table
101
+ })
102
+ except Exception as e:
103
+ return jsonify({'error': str(e)}), 500
104
+
105
+ # Multiple Image Classification Routes
106
+ @app.route('/upload_multiple', methods=['POST'])
107
+ def upload_multiple():
108
+ if 'file' not in request.files:
109
+ return jsonify({'error': 'No file uploaded'}), 400
110
+
111
+ file = request.files['file']
112
+ if not file or not allowed_file(file.filename):
113
+ return jsonify({'error': 'Invalid file type'}), 400
114
+
115
+ try:
116
+ # Save to temp directory
117
+ temp_dir = os.path.join(app.config['UPLOAD_FOLDER_MULTIPLE'], 'temp')
118
+ os.makedirs(temp_dir, exist_ok=True)
119
+
120
+ filename = secure_filename(file.filename)
121
+ filepath = os.path.join(temp_dir, filename)
122
+ file.save(filepath)
123
+
124
+ # Read the file and convert to base64
125
+ with open(filepath, 'rb') as img_file:
126
+ img_data = base64.b64encode(img_file.read()).decode()
127
+
128
+ return jsonify({
129
+ 'filename': filename,
130
+ 'image': img_data,
131
+ 'status': 'Queued'
132
+ })
133
+
134
+ except Exception as e:
135
+ return jsonify({'error': str(e)}), 500
136
+
137
+ @app.route('/classify_multiple', methods=['POST'])
138
+ def classify_multiple():
139
+ temp_dir = os.path.join(app.config['UPLOAD_FOLDER_MULTIPLE'], 'temp')
140
+
141
+ # Get list of files to process
142
+ files = [f for f in os.listdir(temp_dir)
143
+ if os.path.isfile(os.path.join(temp_dir, f)) and allowed_file(f)]
144
+
145
+ results = []
146
+ for filename in files:
147
+ filepath = os.path.join(temp_dir, filename)
148
+
149
+ # Process single image
150
+ classification_result, result_table, failure_labels = classify(filepath)
151
+
152
+ # Move file to appropriate directory
153
+ category_dir = os.path.join(app.config['UPLOAD_FOLDER_MULTIPLE'], classification_result.lower())
154
+ os.makedirs(category_dir, exist_ok=True)
155
+ dest_path = os.path.join(category_dir, filename)
156
+ shutil.move(filepath, dest_path)
157
+
158
+ # Read the processed image
159
+ with open(dest_path, 'rb') as img_file:
160
+ img_data = base64.b64encode(img_file.read()).decode()
161
+
162
+ # Return single result immediately
163
+ result = {
164
+ 'filename': filename,
165
+ 'status': classification_result,
166
+ 'labels': failure_labels,
167
+ 'image': img_data
168
+ }
169
+
170
+ # Use Flask's Response streaming to send results one by one
171
+ return jsonify(result)
172
+
173
+ return jsonify({'message': 'All files processed'})
174
+
175
+ @app.route('/download_multiple/<category>')
176
+ def download_multiple(category):
177
+ if category not in ['pass', 'fail']:
178
+ return jsonify({'error': 'Invalid category'}), 400
179
+
180
+ category_dir = os.path.join(app.config['UPLOAD_FOLDER_MULTIPLE'], category)
181
+ if not os.path.exists(category_dir):
182
+ return jsonify({'error': 'No images found'}), 404
183
+
184
+ memory_file = io.BytesIO()
185
+ with zipfile.ZipFile(memory_file, 'w') as zf:
186
+ for filename in os.listdir(category_dir):
187
+ filepath = os.path.join(category_dir, filename)
188
+ if os.path.isfile(filepath) and allowed_file(filename):
189
+ zf.write(filepath, filename)
190
+
191
+ memory_file.seek(0)
192
+ return send_file(
193
+ memory_file,
194
+ mimetype='application/zip',
195
+ as_attachment=True,
196
+ download_name=f'{category}_images.zip'
197
+ )
198
+
199
+ @app.route('/save_report', methods=['POST'])
200
+ def save_report():
201
+ try:
202
+ data = request.get_json()
203
+ date = data.get('date')
204
+ time = data.get('time')
205
+ summary_content = data.get('summary')
206
+ table_data = data.get('tableData', [])
207
+
208
+ if not all([date, time, summary_content, table_data]):
209
+ return jsonify({'error': 'Invalid data provided'}), 400
210
+
211
+ # Create the table HTML
212
+ table_rows = []
213
+ for item in table_data:
214
+ labels_html = ''
215
+ if item['status'].lower() == 'fail' and item['labels']:
216
+ labels = ' '.join(f'<span class="label">{label}</span>' for label in item['labels'])
217
+ labels_html = f'<div class="labels">{labels}</div>'
218
+
219
+ row = f"""
220
+ <tr>
221
+ <td class="serial">{item['serialNo']}</td>
222
+ <td class="image-col">
223
+ <img src="{item['image']}" alt="{item['filename']}"
224
+ class="thumbnail" onclick="openModal(this)"
225
+ style="cursor: pointer;">
226
+ <div class="filename">{item['filename']}</div>
227
+ </td>
228
+ <td class="result-col">
229
+ <span class="result-{item['status'].lower()}">{item['status']}</span>
230
+ </td>
231
+ <td class="labels-col">
232
+ {labels_html if labels_html else '-'}
233
+ </td>
234
+ </tr>
235
+ """
236
+ table_rows.append(row)
237
+
238
+ # Create directories
239
+ reports_dir = Path('Reports')
240
+ reports_dir.mkdir(exist_ok=True)
241
+ date_dir = reports_dir / date
242
+ date_dir.mkdir(exist_ok=True)
243
+
244
+ # Read the template
245
+ template_path = Path('templates/report_template.html')
246
+ with open(template_path, 'r', encoding='utf-8') as f:
247
+ template = f.read()
248
+
249
+ # Fill in the template
250
+ html_content = template.format(
251
+ date=date,
252
+ time=time,
253
+ summary_content=summary_content,
254
+ table_rows='\n'.join(table_rows)
255
+ )
256
+
257
+ # Save the file
258
+ filename = f'Report_{date}_{time}.html'
259
+ report_path = date_dir / filename
260
+ with open(report_path, 'w', encoding='utf-8') as f:
261
+ f.write(html_content)
262
+
263
+ return jsonify({'success': True})
264
+ except Exception as e:
265
+ return jsonify({'error': str(e)}), 500
266
+
267
+ # Add new route for chunked report saving
268
+ @app.route('/save_report_chunk', methods=['POST'])
269
+ def save_report_chunk():
270
+ try:
271
+ chunk_data = request.get_json()
272
+ if not chunk_data:
273
+ return jsonify({'error': 'No data received'}), 400
274
+
275
+ # Log received data for debugging
276
+ print(f"Received chunk data: {chunk_data.keys()}")
277
+
278
+ # Validate all required fields are present
279
+ required_fields = ['chunkNumber', 'totalChunks', 'date', 'time', 'chunk']
280
+ if not all(field in chunk_data for field in required_fields):
281
+ missing_fields = [field for field in required_fields if field not in chunk_data]
282
+ return jsonify({'error': f'Missing required fields: {", ".join(missing_fields)}'}), 400
283
+
284
+ # Extract and validate data
285
+ try:
286
+ chunk_number = int(chunk_data['chunkNumber'])
287
+ total_chunks = int(chunk_data['totalChunks'])
288
+ date = str(chunk_data['date'])
289
+ time = str(chunk_data['time'])
290
+ chunk = chunk_data['chunk']
291
+ summary = chunk_data.get('summary', '')
292
+ except (ValueError, TypeError) as e:
293
+ return jsonify({'error': f'Invalid data format: {str(e)}'}), 400
294
+
295
+ # Validate chunk data
296
+ if not isinstance(chunk, list):
297
+ return jsonify({'error': 'Chunk must be an array'}), 400
298
+
299
+ # Create directories with error handling
300
+ try:
301
+ reports_dir = Path('Reports')
302
+ reports_dir.mkdir(exist_ok=True)
303
+ date_dir = reports_dir / date
304
+ date_dir.mkdir(exist_ok=True)
305
+ temp_dir = date_dir / 'temp'
306
+ temp_dir.mkdir(exist_ok=True)
307
+ except Exception as e:
308
+ return jsonify({'error': f'Failed to create directories: {str(e)}'}), 500
309
+
310
+ # Save chunk with error handling
311
+ try:
312
+ chunk_file = temp_dir / f'chunk_{chunk_number}.json'
313
+ chunk_data_to_save = {
314
+ 'data': chunk,
315
+ 'summary': summary if chunk_number == 0 else ''
316
+ }
317
+ with open(chunk_file, 'w', encoding='utf-8') as f:
318
+ json.dump(chunk_data_to_save, f)
319
+ except Exception as e:
320
+ return jsonify({'error': f'Failed to save chunk: {str(e)}'}), 500
321
+
322
+ # Process final chunk
323
+ if chunk_number == total_chunks - 1:
324
+ try:
325
+ # Collect all chunks
326
+ all_data = []
327
+ summary_content = ''
328
+
329
+ # Verify all chunks exist
330
+ for i in range(total_chunks):
331
+ chunk_file = temp_dir / f'chunk_{i}.json'
332
+ if not chunk_file.exists():
333
+ return jsonify({'error': f'Missing chunk file: chunk_{i}.json'}), 500
334
+
335
+ with open(chunk_file, 'r', encoding='utf-8') as f:
336
+ chunk_content = json.load(f)
337
+ all_data.extend(chunk_content['data'])
338
+ if chunk_content['summary']:
339
+ summary_content = chunk_content['summary']
340
+
341
+ # Verify template exists
342
+ template_path = Path('templates/report_template.html')
343
+ multiple_path = Path('templates/multiple.html')
344
+
345
+ if not template_path.exists() or not multiple_path.exists():
346
+ return jsonify({'error': 'Template files not found'}), 500
347
+
348
+ # Copy the entire multiple.html content and remove upload section
349
+ with open(multiple_path, 'r', encoding='utf-8') as f:
350
+ template = f.read()
351
+ # Remove the upload section from template
352
+ upload_start = template.find('<div class="upload-section">')
353
+ upload_end = template.find('</div>', upload_start) + 6
354
+ template = template.replace(template[upload_start:upload_end], '')
355
+ # Replace placeholders
356
+ template = template.replace('<h1>Multiple Images Classification</h1>', '<h1>Classification Report</h1>')
357
+ template = template.replace('<p>Classify and segregate multiple images at once...</p>', '<p>{date} {time}</p>')
358
+ template = template.replace('<div id="summary"></div>', '<div id="summary">{summary_content}</div>')
359
+ template = template.replace('<tbody id="results-tbody">', '<tbody>{table_rows}</tbody>')
360
+
361
+ # Extract styles from multiple.html
362
+ with open(multiple_path, 'r', encoding='utf-8') as f:
363
+ multiple_content = f.read()
364
+ style_start = multiple_content.find('<style>') + 7
365
+ style_end = multiple_content.find('</style>')
366
+ styles = multiple_content[style_start:style_end].strip()
367
+
368
+ # Read and process the report template
369
+ with open(template_path, 'r', encoding='utf-8') as f:
370
+ template = f.read()
371
+ # Replace style placeholder with actual styles
372
+ template = template.replace('{multiple_styles}', styles)
373
+ # Replace single braces with double braces to escape them
374
+ template = template.replace('{', '{{').replace('}', '}}')
375
+ # Restore our format placeholders
376
+ template = template.replace('{{date}}', '{date}') \
377
+ .replace('{{time}}', '{time}') \
378
+ .replace('{{summary_content}}', '{summary_content}') \
379
+ .replace('{{table_rows}}', '{table_rows}')
380
+
381
+ # Create table rows with escaped curly braces (only failed items)
382
+ table_rows = []
383
+ serial_counter = 1 # Keep track of numbering
384
+
385
+ for item in all_data:
386
+ try:
387
+ if item['status'].lower() == 'fail': # Only process failed items
388
+ labels_html = ''
389
+ if item['labels']:
390
+ labels = ' '.join(f'<span class="label">{label}</span>'
391
+ for label in item['labels'])
392
+ labels_html = f'<div class="labels">{labels}</div>'
393
+
394
+ row = f"""
395
+ <tr>
396
+ <td class="serial">{serial_counter}</td>
397
+ <td class="image-col">
398
+ <img src="{item['image']}" alt="{item['filename']}"
399
+ class="thumbnail" onclick="openModal(this)"
400
+ style="cursor: pointer;">
401
+ <div class="filename">{item['filename']}</div>
402
+ </td>
403
+ <td class="result-col">
404
+ <span class="result-fail">{item['status']}</span>
405
+ </td>
406
+ <td class="labels-col">
407
+ {labels_html if labels_html else '-'}
408
+ </td>
409
+ </tr>
410
+ """
411
+ table_rows.append(row)
412
+ serial_counter += 1
413
+
414
+ except KeyError as e:
415
+ return jsonify({'error': f'Missing required field in data: {str(e)}'}), 500
416
+
417
+ # Generate final HTML
418
+ try:
419
+ html_content = template.format(
420
+ date=date,
421
+ time=time,
422
+ summary_content=summary_content,
423
+ table_rows='\n'.join(table_rows)
424
+ )
425
+ except Exception as e:
426
+ return jsonify({'error': f'Failed to generate HTML: {str(e)}'}), 500
427
+
428
+ # Save final report
429
+ try:
430
+ filename = f'Report_{date}_{time}.html'
431
+ report_path = date_dir / filename
432
+ with open(report_path, 'w', encoding='utf-8') as f:
433
+ f.write(html_content)
434
+ except Exception as e:
435
+ return jsonify({'error': f'Failed to save report: {str(e)}'}), 500
436
+
437
+ # Cleanup
438
+ try:
439
+ shutil.rmtree(temp_dir)
440
+ except Exception as e:
441
+ print(f"Warning: Failed to clean up temp directory: {str(e)}")
442
+
443
+ return jsonify({'success': True})
444
+
445
+ except Exception as e:
446
+ return jsonify({'error': f'Error processing final chunk: {str(e)}'}), 500
447
+
448
+ return jsonify({'success': True, 'message': 'Chunk received'})
449
+
450
+ except Exception as e:
451
+ print(f"Error in save_report_chunk: {str(e)}")
452
+ return jsonify({'error': str(e)}), 500
453
+
454
+ if __name__ == '__main__':
455
+ # Clear uploads on startup
456
+ clear_uploads(app.config['UPLOAD_FOLDER_SINGLE'])
457
+ clear_uploads(app.config['UPLOAD_FOLDER_MULTIPLE'])
458
+ app.run(host='0.0.0.0', port=7860, debug=False)
config.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # ROI Constants
3
+ BODY = (0.0, 0.0, 1.0, 1.0)
4
+ TAG = (0.05, 0.62, 1.0, 0.65)
5
+ DTAG = (0.05, 0.592, 1.0, 0.622)
6
+ TNC = (0.02, 0.98, 1.0, 1.0)
7
+ CTA = (0.68, 0.655, 0.87, 0.675)
8
+ GNC = (0.5, 0.652, 0.93, 0.77)
9
+
10
+ # ROIs for Ribbon Detection
11
+ ROIS = [
12
+ # Top section divided into 3 parts
13
+ (0.0, 0.612, 0.33, 0.626), # Top left
14
+ (0.33, 0.612, 0.66, 0.626), # Top middle
15
+ (0.66, 0.612, 1.0, 0.626), # Top right
16
+
17
+ # Bottom section divided into 3 parts
18
+ (0.0, 0.678, 0.33, 0.686), # Bottom left
19
+ (0.33, 0.678, 0.66, 0.686), # Bottom middle
20
+ (0.66, 0.678, 1.0, 0.686), # Bottom right
21
+
22
+ # Extreme Right section
23
+ (0.95, 0.63, 1, 0.678),
24
+
25
+ # Middle Section (between Tag and Click)
26
+ (0.029, 0.648, 0.35, 0.658), # Middle left
27
+ (0.35, 0.648, 0.657, 0.658) # Middle right
28
+ ]
29
+
30
+ # Detection parameters for Ribbon
31
+ DETECTION_PARAMS = {
32
+ 'clahe_clip_limit': 2.0,
33
+ 'clahe_grid_size': (8, 8),
34
+ 'gaussian_kernel': (5, 5),
35
+ 'gaussian_sigma': 0,
36
+ 'canny_low': 20,
37
+ 'canny_high': 80,
38
+ 'hough_threshold': 15,
39
+ 'min_line_length': 10,
40
+ 'max_line_gap': 5,
41
+ 'edge_pixel_threshold': 0.01
42
+ }
43
+
44
+ # Prompts
45
+ PTAG = "Extract all the text from the image accurately."
46
+ PEMO = "Carefully analyze the image to detect emojis. Emojis are graphical icons (e.g., 😀, 🎉, ❤️) and not regular text, symbols, or characters. Examine the image step by step to ensure only graphical emojis are counted. If no emojis are found, respond with 'NUMBER OF EMOJIS: 0'. If emojis are present, count them and provide reasoning before giving the final answer in the format 'NUMBER OF EMOJIS: [count]'. Do not count text or punctuation as emojis."
47
+ PGNC = "Is there a HAND POINTER/EMOJI or a LARGE ARROW or ARROW POINTER? Answer only 'yes' or 'no'."
48
+
49
+ # Lists for Content Checks
50
+ RISKY_KEYWORDS = [
51
+ # General gambling terms
52
+ "casino", "poker", "jackpot", "blackjack",
53
+ "sports betting", "online casino", "slot machine", "pokies",
54
+
55
+ # Gambling website and app names (Global and India-promoted)
56
+ "stake", "betano", "bet365", "888casino", "ladbrokes", "betfair",
57
+ "unibet", "skybet", "coral", "betway", "sportingbet", "betvictor", "partycasino", "casinocom", "jackpot city",
58
+ "playtech", "meccabingo", "fanDuel", "betmobile", "10bet", "10cric",
59
+ "pokerstars" "fulltiltpoker", "wsop",
60
+
61
+ # Gambling websites and apps promoted or popular in India
62
+ "dream11", "dreamll", "my11circle", "cricbuzz", "fantasy cricket", "sportz exchange", "fun88",
63
+ "funbb", "funbeecom", "funbee", "rummycircle", "pokertiger", "adda52", "khelplay",
64
+ "paytm first games", "fanmojo", "betking", "1xbet", "parimatch", "rajapoker",
65
+
66
+ # High-risk trading and investment terms
67
+ "win cash", "high risk trading", "win lottery",
68
+ "high risk investment", "investment scheme",
69
+ "get rich quick", "trading signals", "financial markets", "day trading",
70
+ "options trading", "forex signals"
71
+ ]
72
+
73
+ ILLEGAL_ACTIVITIES = [
74
+ "hack", "hacking", "cheating", "cheat", "drugs", "drug", "steal", "stealing",
75
+ "phishing", "phish", "piracy", "pirate", "fraud", "smuggling", "smuggle",
76
+ "counterfeiting", "blackmailing", "blackmail", "extortion", "scamming", "scam",
77
+ "identity theft", "illegal trading", "money laundering", "poaching", "poach",
78
+ "trafficking", "illegal arms", "explosives", "bomb", "bombing", "fake documents"
79
+ ]
80
+
81
+ ILLEGAL_PHRASES = [
82
+ "how to", "learn", "steps to", "guide to", "ways to",
83
+ "tutorial on", "methods for", "process of",
84
+ "tricks for", "shortcuts to", "make"
85
+ ]
86
+
87
+ COMPETITOR_BRANDS = [
88
+ "motorola", "oppo", "vivo", "htc", "sony", "nokia", "honor", "huawei", "asus", "lg",
89
+ "oneplus", "apple", "micromax", "lenovo", "gionee", "infocus", "lava", "panasonic","intex",
90
+ "blackberry", "xiaomi", "philips", "godrej", "whirlpool", "blue star", "voltas",
91
+ "hitachi", "realme", "poco", "iqoo", "toshiba", "skyworth", "redmi", "nokia", "lava"
92
+ ]
93
+
94
+ APPROPRIATE_LABELS = [
95
+ "Inappropriate Content: Violence, Blood, political promotion, drugs, alcohol, cigarettes, smoking, cruelty, nudity, illegal activities",
96
+ "Appropriate Content: Games, entertainment, Advertisement, Fashion, Sun-glasses, Food, Food Ad, Fast Food, Woman or Man Model, Television, natural scenery, abstract visuals, art, everyday objects, sports, news, general knowledge, medical symbols, and miscellaneous benign content"
97
+ ]
98
+
99
+ RELIGIOUS_LABELS = [
100
+ "Digital art or sports or news or miscellaneous activity or miscellaneous item or Person or religious places or diya or deepak or festival or nature or earth imagery or scenery or Medical Plus Sign or Violence or Military",
101
+ "Hindu Deity / OM or AUM or Swastik symbol",
102
+ "Jesus Christ / Christianity Cross"
103
+ ]
104
+
105
+ # Image Quality Thresholds
106
+ MIN_WIDTH = 720
107
+ MIN_HEIGHT = 1600
108
+ MIN_PIXEL_COUNT = 1000000
109
+ PIXEL_VARIANCE_THRESHOLD = 50
model_handler.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import easyocr
4
+ import numpy as np
5
+ import gc
6
+ from transformers import AutoTokenizer, AutoModel, AutoProcessor, AutoModelForZeroShotImageClassification
7
+ import torch.nn.functional as F
8
+ from utils import build_transform
9
+
10
+ class ModelHandler:
11
+ def __init__(self):
12
+ self.device = torch.device("cpu") # Change to "cuda" if GPU available
13
+ self.transform = build_transform()
14
+ self.load_models()
15
+
16
+ def load_models(self):
17
+ # MODEL 1: InternVL
18
+ try:
19
+ # Check if local path exists, otherwise use HF Hub ID
20
+ local_path = os.path.join("Models", "InternVL2_5-1B-MPO")
21
+ if os.path.exists(local_path):
22
+ internvl_model_path = local_path
23
+ print(f"Loading InternVL from local path: {internvl_model_path}")
24
+ else:
25
+ internvl_model_path = "OpenGVLab/InternVL2_5-1B-MPO" # HF Hub ID
26
+ print(f"Local model not found. Downloading InternVL from HF Hub: {internvl_model_path}")
27
+
28
+ self.model_int = AutoModel.from_pretrained(
29
+ internvl_model_path,
30
+ torch_dtype=torch.bfloat16,
31
+ low_cpu_mem_usage=True,
32
+ trust_remote_code=True
33
+ ).eval()
34
+
35
+ for module in self.model_int.modules():
36
+ if isinstance(module, torch.nn.Dropout):
37
+ module.p = 0
38
+
39
+ self.tokenizer_int = AutoTokenizer.from_pretrained(internvl_model_path, trust_remote_code=True)
40
+ print("\nInternVL model and tokenizer loaded successfully.")
41
+ except Exception as e:
42
+ print(f"\nError loading InternVL model or tokenizer: {e}")
43
+ self.model_int = None
44
+ self.tokenizer_int = None
45
+
46
+ # MODEL 2: EasyOCR
47
+ try:
48
+ # EasyOCR automatically handles downloading if not present
49
+ self.reader = easyocr.Reader(['en', 'hi'], gpu=False)
50
+ print("\nEasyOCR reader initialized successfully.")
51
+ except Exception as e:
52
+ print(f"\nError initializing EasyOCR reader: {e}")
53
+ self.reader = None
54
+
55
+ # MODEL 3: CLIP
56
+ try:
57
+ local_path = os.path.join("Models", "clip-vit-base-patch32")
58
+ if os.path.exists(local_path):
59
+ clip_model_path = local_path
60
+ print(f"Loading CLIP from local path: {clip_model_path}")
61
+ else:
62
+ clip_model_path = "openai/clip-vit-base-patch32" # HF Hub ID
63
+ print(f"Local model not found. Downloading CLIP from HF Hub: {clip_model_path}")
64
+
65
+ self.processor_clip = AutoProcessor.from_pretrained(clip_model_path)
66
+ self.model_clip = AutoModelForZeroShotImageClassification.from_pretrained(clip_model_path).to(self.device)
67
+ print("\nCLIP model and processor loaded successfully.")
68
+ except Exception as e:
69
+ print(f"\nError loading CLIP model or processor: {e}")
70
+ self.model_clip = None
71
+ self.processor_clip = None
72
+
73
+ def easyocr_ocr(self, image):
74
+ if not self.reader:
75
+ return ""
76
+ image_np = np.array(image)
77
+ results = self.reader.readtext(image_np, detail=1)
78
+
79
+ del image_np
80
+ gc.collect()
81
+
82
+ if not results:
83
+ return ""
84
+
85
+ sorted_results = sorted(results, key=lambda x: (x[0][0][1], x[0][0][0]))
86
+ ordered_text = " ".join([res[1] for res in sorted_results]).strip()
87
+ return ordered_text
88
+
89
+ def intern(self, image, prompt, max_tokens):
90
+ if not self.model_int or not self.tokenizer_int:
91
+ return ""
92
+
93
+ pixel_values = self.transform(image).unsqueeze(0).to(self.device).to(torch.bfloat16)
94
+ with torch.no_grad():
95
+ response, _ = self.model_int.chat(
96
+ self.tokenizer_int,
97
+ pixel_values,
98
+ prompt,
99
+ generation_config={
100
+ "max_new_tokens": max_tokens,
101
+ "do_sample": False,
102
+ "num_beams": 1,
103
+ "temperature": 1.0,
104
+ "top_p": 1.0,
105
+ "repetition_penalty": 1.0,
106
+ "length_penalty": 1.0,
107
+ "pad_token_id": self.tokenizer_int.pad_token_id
108
+ },
109
+ history=None,
110
+ return_history=True
111
+ )
112
+
113
+ del pixel_values
114
+ gc.collect()
115
+ return response
116
+
117
+ def clip(self, image, labels):
118
+ if not self.model_clip or not self.processor_clip:
119
+ return None
120
+
121
+ processed = self.processor_clip(
122
+ text=labels,
123
+ images=image,
124
+ padding=True,
125
+ return_tensors="pt"
126
+ ).to(self.device)
127
+
128
+ del image, labels
129
+ gc.collect()
130
+ return processed
131
+
132
+ def get_clip_probs(self, image, labels):
133
+ inputs = self.clip(image, labels)
134
+ if inputs is None:
135
+ return None
136
+
137
+ with torch.no_grad():
138
+ outputs = self.model_clip(**inputs)
139
+
140
+ logits_per_image = outputs.logits_per_image
141
+ probs = F.softmax(logits_per_image, dim=1)
142
+
143
+ del inputs, outputs, logits_per_image
144
+ gc.collect()
145
+
146
+ return probs
147
+
148
+ # Create a global instance to be used by modules
149
+ model_handler = ModelHandler()
modules/__init__.py ADDED
File without changes
modules/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (162 Bytes). View file
 
modules/__pycache__/content_checks.cpython-311.pyc ADDED
Binary file (4.59 kB). View file
 
modules/__pycache__/text_checks.cpython-311.pyc ADDED
Binary file (7.75 kB). View file
 
modules/__pycache__/visual_checks.cpython-311.pyc ADDED
Binary file (5.92 kB). View file
 
modules/content_checks.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import torch
3
+ from PIL import Image
4
+ import config
5
+ from utils import find_similar_substring, destroy_text_roi
6
+ from model_handler import model_handler
7
+
8
+ def is_risky(body_text):
9
+ body_text = re.sub(r'[^a-zA-Z0-9\u0966-\u096F\s]', '', body_text)
10
+ for keyword in config.RISKY_KEYWORDS:
11
+ if find_similar_substring(body_text, keyword):
12
+ return True
13
+ return False
14
+
15
+ def is_prom_illegal_activity(body_text):
16
+ for phrase in config.ILLEGAL_PHRASES:
17
+ for activity in config.ILLEGAL_ACTIVITIES:
18
+ pattern = rf"{re.escape(phrase)}.*?{re.escape(activity)}"
19
+ if re.search(pattern, body_text):
20
+ return True
21
+ return False
22
+
23
+ def is_competitor(body_text):
24
+ for brand in config.COMPETITOR_BRANDS:
25
+ if re.search(r'\b' + re.escape(brand) + r'\b', body_text):
26
+ return True
27
+ return False
28
+
29
+ def body(image_path):
30
+ results = {}
31
+ image = Image.open(image_path).convert('RGB')
32
+ bd = model_handler.intern(image, config.PTAG, 500).lower()
33
+ ocr_substitutions = {'0': 'o', '1': 'l', '!': 'l', '@': 'a', '5': 's', '8': 'b'}
34
+
35
+ for char, substitute in ocr_substitutions.items():
36
+ bd = bd.replace(char, substitute)
37
+ bd = ' '.join(bd.split())
38
+
39
+ results["High Risk Content"] = 1 if is_risky(bd) else 0
40
+ results["Illegal Content"] = 1 if is_prom_illegal_activity(bd) else 0
41
+ results["Competitor References"] = 1 if is_competitor(bd) else 0
42
+
43
+ return results
44
+
45
+ def offensive(image):
46
+ image = destroy_text_roi(image, *config.TAG)
47
+
48
+ probs = model_handler.get_clip_probs(image, config.APPROPRIATE_LABELS)
49
+ if probs is None:
50
+ return False
51
+
52
+ inappropriate_prob = probs[0][0].item()
53
+ appropriate_prob = probs[0][1].item()
54
+
55
+ if inappropriate_prob > appropriate_prob:
56
+ return True
57
+ return False
58
+
59
+ def religious(image):
60
+ probs = model_handler.get_clip_probs(image, config.RELIGIOUS_LABELS)
61
+ if probs is None:
62
+ return False, None
63
+
64
+ highest_score_index = torch.argmax(probs, dim=1).item()
65
+
66
+ if highest_score_index != 0:
67
+ return True, config.RELIGIOUS_LABELS[highest_score_index]
68
+ return False, None
69
+
70
+ def theme(image_path):
71
+ results = {}
72
+ image = Image.open(image_path).convert('RGB')
73
+
74
+ results["Inappropriate Content"] = 1 if offensive(image) else 0
75
+
76
+ is_religious, religious_label = religious(image)
77
+ results["Religious Content"] = f"1 [{religious_label}]" if is_religious else "0"
78
+
79
+ return results
modules/text_checks.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import emoji
3
+ from PIL import Image
4
+ import config
5
+ from utils import get_roi, clean_text, are_strings_similar, blur_image, is_blank, is_english, is_valid_english, destroy_text_roi
6
+ from model_handler import model_handler
7
+
8
+ def is_unreadable_tagline(htag, tag):
9
+ clean_htag = clean_text(htag)
10
+ clean_tag = clean_text(tag)
11
+ return not are_strings_similar(clean_htag, clean_tag)
12
+
13
+ def is_hyperlink_tagline(tag):
14
+ substrings = ['www', '.com', 'http']
15
+ return any(sub in tag for sub in substrings)
16
+
17
+ def is_price_tagline(tag):
18
+ exclude_keywords = ["crore", "thousand", "million", "billion", "trillion"]
19
+ exclude_pattern = r'(₹\.?\s?\d+\s*(lac|lacs|lakh|lakhs|cr|k))|(\brs\.?\s?\d+\s*(lac|lacs|lakh|lakhs|cr|k))|(\$\.?\s?\d+\s*(lac|lacs|lakh|lakhs|cr|k))'
20
+ price_pattern = r'(₹\s?\d+)|(\brs\.?\s?\d+)|(\$\s?\d+)|(र\d+)'
21
+
22
+ if any(keyword in tag for keyword in exclude_keywords):
23
+ return False
24
+ if re.search(exclude_pattern, tag):
25
+ return False
26
+ return bool(re.search(price_pattern, tag))
27
+
28
+ def is_multiple_emoji(emoji_text):
29
+ words = emoji_text.split()
30
+ last_word = words[-1]
31
+ return last_word not in ['0', '1']
32
+
33
+ def is_incomplete_tagline(tag, is_eng):
34
+ tag = emoji.replace_emoji(tag, '')
35
+ tag = tag.strip()
36
+ if tag.endswith(('...', '..')):
37
+ return True
38
+ if not is_eng and tag.endswith(('.')):
39
+ return True
40
+ return False
41
+
42
+ def tagline(image_path):
43
+ results = {
44
+ "Empty/Illegible/Black Tagline": 0,
45
+ "Multiple Taglines": 0,
46
+ "Incomplete Tagline": 0,
47
+ "Hyperlink": 0,
48
+ "Price Tag": 0,
49
+ "Excessive Emojis": 0
50
+ }
51
+
52
+ image = get_roi(image_path, *config.TAG)
53
+ himage = blur_image(image, 0.3)
54
+ easytag = model_handler.easyocr_ocr(image).lower().strip()
55
+ unr = model_handler.easyocr_ocr(himage).lower().strip()
56
+
57
+ if is_blank(easytag) or is_blank(unr):
58
+ results["Empty/Illegible/Black Tagline"] = 1
59
+ return results
60
+
61
+ is_eng = is_english(easytag)
62
+ if not is_eng:
63
+ results["Empty/Illegible/Black Tagline"] = 0
64
+ tag = easytag
65
+ else:
66
+ Tag = model_handler.intern(image, config.PTAG, 25).strip()
67
+ tag = Tag.lower()
68
+
69
+ htag = model_handler.intern(himage, config.PTAG, 25).lower().strip()
70
+ if is_unreadable_tagline(htag, tag):
71
+ results["Empty/Illegible/Black Tagline"] = 1
72
+
73
+ results["Incomplete Tagline"] = 1 if is_incomplete_tagline(tag, is_eng) else 0
74
+ results["Hyperlink"] = 1 if is_hyperlink_tagline(tag) else 0
75
+ results["Price Tag"] = 1 if is_price_tagline(tag) else 0
76
+
77
+ imagedt = get_roi(image_path, *config.DTAG)
78
+ dtag = model_handler.easyocr_ocr(imagedt).strip()
79
+ results["Multiple Taglines"] = 0 if is_blank(dtag) else 1
80
+
81
+ emoji_resp = model_handler.intern(image, config.PEMO, 100)
82
+ results["Excessive Emojis"] = 1 if is_multiple_emoji(emoji_resp) else 0
83
+
84
+ return results
85
+
86
+ def cta(image_path):
87
+ image = get_roi(image_path, *config.CTA)
88
+ cta_text = model_handler.intern(image, config.PTAG, 5).strip()
89
+ veng = is_valid_english(cta_text)
90
+ eng = is_english(cta_text)
91
+
92
+ if '.' in cta_text or '..' in cta_text or '...' in cta_text:
93
+ return {"Bad CTA": 1}
94
+
95
+ if any(emoji.is_emoji(c) for c in cta_text):
96
+ return {"Bad CTA": 1}
97
+
98
+ clean_cta_text = clean_text(cta_text)
99
+ # print(len(clean_cta_text)) # Removed print
100
+
101
+ if eng and len(clean_cta_text) <= 2:
102
+ return {"Bad CTA": 1}
103
+
104
+ if len(clean_cta_text) > 15:
105
+ return {"Bad CTA": 1}
106
+
107
+ return {"Bad CTA": 0}
108
+
109
+ def tnc(image_path):
110
+ image = get_roi(image_path, *config.TNC)
111
+ tnc_text = model_handler.easyocr_ocr(image)
112
+ clean_tnc = clean_text(tnc_text)
113
+
114
+ return {"Terms & Conditions": 0 if is_blank(clean_tnc) else 1}
115
+
116
+ def tooMuchText(image_path):
117
+ DRIB = (0.04, 0.625, 1.0, 0.677)
118
+ DUP = (0, 0, 1.0, 0.25)
119
+ DBEL = (0, 0.85, 1.0, 1)
120
+ image = Image.open(image_path).convert('RGB')
121
+ image = destroy_text_roi(image, *DRIB)
122
+ image = destroy_text_roi(image, *DUP)
123
+ image = destroy_text_roi(image, *DBEL)
124
+ bd = model_handler.easyocr_ocr(image).lower().strip()
125
+ return {"Too Much Text": 1 if len(bd) > 55 else 0}
modules/visual_checks.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ from PIL import Image
4
+ import config
5
+ from utils import get_roi
6
+ from model_handler import model_handler
7
+
8
+ def detect_straight_lines(roi_img):
9
+ """Enhanced edge detection focusing on straight lines."""
10
+ gray = cv2.cvtColor(roi_img, cv2.COLOR_BGR2GRAY)
11
+ clahe = cv2.createCLAHE(
12
+ clipLimit=config.DETECTION_PARAMS['clahe_clip_limit'],
13
+ tileGridSize=config.DETECTION_PARAMS['clahe_grid_size']
14
+ )
15
+ enhanced = clahe.apply(gray)
16
+ blurred = cv2.GaussianBlur(
17
+ enhanced,
18
+ config.DETECTION_PARAMS['gaussian_kernel'],
19
+ config.DETECTION_PARAMS['gaussian_sigma']
20
+ )
21
+ edges = cv2.Canny(
22
+ blurred,
23
+ config.DETECTION_PARAMS['canny_low'],
24
+ config.DETECTION_PARAMS['canny_high']
25
+ )
26
+ line_mask = np.zeros_like(edges)
27
+ lines = cv2.HoughLinesP(
28
+ edges,
29
+ rho=1,
30
+ theta=np.pi/180,
31
+ threshold=config.DETECTION_PARAMS['hough_threshold'],
32
+ minLineLength=config.DETECTION_PARAMS['min_line_length'],
33
+ maxLineGap=config.DETECTION_PARAMS['max_line_gap']
34
+ )
35
+ if lines is not None:
36
+ for line in lines:
37
+ x1, y1, x2, y2 = line[0]
38
+ cv2.line(line_mask, (x1, y1), (x2, y2), 255, 2)
39
+ return line_mask
40
+
41
+ def simple_edge_detection(roi_img):
42
+ """Simple edge detection."""
43
+ gray = cv2.cvtColor(roi_img, cv2.COLOR_BGR2GRAY)
44
+ return cv2.Canny(gray, 50, 150)
45
+
46
+ def ribbon(image_path):
47
+ """Detect the presence of a ribbon in an image."""
48
+ image = cv2.imread(image_path)
49
+ if image is None:
50
+ raise ValueError(f"Could not read image: {image_path}")
51
+
52
+ h, w = image.shape[:2]
53
+ edge_present = []
54
+
55
+ for i, roi in enumerate(config.ROIS):
56
+ x1, y1, x2, y2 = [int(coord * (w if i % 2 == 0 else h)) for i, coord in enumerate(roi)]
57
+ roi_img = image[y1:y2, x1:x2]
58
+
59
+ if i < 6: # Straight line detection for ROIs 0-5
60
+ edges = detect_straight_lines(roi_img)
61
+ edge_present.append(np.sum(edges) > edges.size * config.DETECTION_PARAMS['edge_pixel_threshold'])
62
+ else: # Original method for ROIs 6-8
63
+ edges = simple_edge_detection(roi_img)
64
+ edge_present.append(np.any(edges))
65
+
66
+ result = all(edge_present[:6]) and not edge_present[6] and not edge_present[7] and not edge_present[8]
67
+ return {"No Ribbon": 0 if result else 1}
68
+
69
+ def image_quality(image_path):
70
+ """
71
+ Check if an image is low resolution or poor quality.
72
+ """
73
+ try:
74
+ image = Image.open(image_path)
75
+ width, height = image.size
76
+ pixel_count = width * height
77
+
78
+ if width < config.MIN_WIDTH or height < config.MIN_HEIGHT or pixel_count < config.MIN_PIXEL_COUNT:
79
+ return {"Bad Image Quality": 1}
80
+
81
+ grayscale_image = image.convert("L")
82
+ pixel_array = np.array(grayscale_image)
83
+ variance = np.var(pixel_array)
84
+
85
+ if variance < config.PIXEL_VARIANCE_THRESHOLD:
86
+ return {"Bad Image Quality": 1}
87
+
88
+ return {"Bad Image Quality": 0}
89
+
90
+ except Exception as e:
91
+ print(f"Error processing image: {e}")
92
+ return {"Bad Image Quality": 1}
93
+
94
+ def gnc(image_path):
95
+ """Check for gestures/coach marks and display the image."""
96
+ image = get_roi(image_path, *config.GNC)
97
+ gnc_text = model_handler.intern(image, config.PGNC, 900).lower()
98
+
99
+ return {"Visual Gesture or Icon": 1 if 'yes' in gnc_text else 0}
pipeline.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tabulate import tabulate
2
+ from modules import visual_checks, text_checks, content_checks
3
+
4
+ def classify(image_path):
5
+ """Perform complete classification with detailed results."""
6
+ # Components to check
7
+ components = [
8
+ visual_checks.image_quality,
9
+ visual_checks.ribbon,
10
+ text_checks.tagline,
11
+ text_checks.tooMuchText,
12
+ content_checks.theme,
13
+ content_checks.body,
14
+ text_checks.cta,
15
+ text_checks.tnc,
16
+ visual_checks.gnc
17
+ ]
18
+
19
+ # Collect all results
20
+ all_results = {}
21
+ for component in components:
22
+ try:
23
+ results = component(image_path)
24
+ all_results.update(results)
25
+ except Exception as e:
26
+ print(f"Error in component {component.__name__}: {e}")
27
+ # Optionally set default values or log error
28
+ pass
29
+
30
+ # Calculate final classification
31
+ # Check if any result value is 1 (or starts with '1' for string results like "1 [Religious]")
32
+ final_classification = 0
33
+ for result in all_results.values():
34
+ if isinstance(result, int):
35
+ if result == 1:
36
+ final_classification = 1
37
+ break
38
+ elif isinstance(result, str):
39
+ if result.startswith('1'):
40
+ final_classification = 1
41
+ break
42
+
43
+ # Determine Pass or Fail
44
+ classification_result = "Fail" if final_classification == 1 else "Pass"
45
+
46
+ # Prepare the table data
47
+ table_data = []
48
+ labels = [
49
+ "Bad Image Quality", "No Ribbon", "Empty/Illegible/Black Tagline", "Multiple Taglines",
50
+ "Incomplete Tagline", "Hyperlink", "Price Tag", "Excessive Emojis", "Too Much Text",
51
+ "Inappropriate Content", "Religious Content", "High Risk Content",
52
+ "Illegal Content", "Competitor References", "Bad CTA", "Terms & Conditions",
53
+ "Visual Gesture or Icon"
54
+ ]
55
+
56
+ # Collect labels responsible for failure
57
+ failure_labels = []
58
+ for label in labels:
59
+ result = all_results.get(label, 0)
60
+
61
+ is_fail = False
62
+ if isinstance(result, int) and result == 1:
63
+ is_fail = True
64
+ elif isinstance(result, str) and result.startswith('1'):
65
+ is_fail = True
66
+
67
+ if is_fail:
68
+ failure_labels.append(label)
69
+
70
+ table_data.append([label, result])
71
+
72
+ # Format the results as a table
73
+ result_table = tabulate(table_data, headers=["LABEL", "RESULT"], tablefmt="fancy_grid")
74
+
75
+ # Return the final classification, result table, and failure labels (if any)
76
+ return classification_result, result_table, failure_labels
77
+
78
+ # Dummy interface for testing (can be enabled if needed)
79
+ def classify_dummy(image_path):
80
+ import random
81
+ all_results = {
82
+ "Bad Image Quality": 0,
83
+ "No Ribbon": random.choice([0, 1]),
84
+ "Empty/Illegible/Black Tagline": 0,
85
+ "Multiple Taglines": 0,
86
+ "Incomplete Tagline": 0,
87
+ "Hyperlink": 0,
88
+ "Price Tag": 0,
89
+ "Excessive Emojis": 0,
90
+ "Too Much Text": 0,
91
+ "Inappropriate Content": 0,
92
+ "Religious Content": 0,
93
+ "High Risk Content": 0,
94
+ "Illegal Content": 0,
95
+ "Competitor References": 0,
96
+ "Bad CTA": 0,
97
+ "Terms & Conditions": 0,
98
+ "Visual Gesture or Icon": 0
99
+ }
100
+
101
+ final_classification = 1 if any(result == 1 for result in all_results.values()) else 0
102
+ classification_result = "Fail" if final_classification == 1 else "Pass"
103
+
104
+ table_data = []
105
+ labels = list(all_results.keys())
106
+ failure_labels = [label for label in labels if all_results[label] == 1]
107
+
108
+ for label in labels:
109
+ table_data.append([label, all_results[label]])
110
+
111
+ result_table = tabulate(table_data, headers=["LABEL", "RESULT"], tablefmt="fancy_grid")
112
+ return classification_result, result_table, failure_labels
requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ easyocr
2
+ fuzzywuzzy
3
+ emoji
4
+ torch
5
+ torchvision
6
+ transformers
7
+ matplotlib
8
+ pillow
9
+ opencv-python-headless
10
+ tabulate
11
+ nltk
12
+ einops
13
+ timm
14
+ accelerate
15
+ flask
16
+ python-Levenshtein
templates/index.html ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>PRISM Project</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
8
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css" rel="stylesheet">
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
11
+
12
+ body {
13
+ font-family: 'Poppins', sans-serif;
14
+ min-height: 100vh;
15
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
16
+ display: flex;
17
+ justify-content: center;
18
+ align-items: center;
19
+ margin: 0;
20
+ color: white;
21
+ overflow-x: hidden;
22
+ }
23
+
24
+ .container {
25
+ position: relative;
26
+ z-index: 1;
27
+ width: 100%;
28
+ max-width: 1000px;
29
+ margin: 0 auto;
30
+ padding: 2rem;
31
+ }
32
+
33
+ .glass-card {
34
+ background: rgba(255, 255, 255, 0.1);
35
+ backdrop-filter: blur(10px);
36
+ border-radius: 20px;
37
+ padding: 4rem;
38
+ box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
39
+ border: 1px solid rgba(255, 255, 255, 0.18);
40
+ transform: translateY(0);
41
+ transition: transform 0.3s ease;
42
+ text-align: center;
43
+ }
44
+
45
+ .glass-card:hover {
46
+ transform: translateY(-5px);
47
+ }
48
+
49
+ .btn-container {
50
+ display: flex;
51
+ justify-content: space-between;
52
+ padding: 0 4rem;
53
+ margin-top: 3rem;
54
+ }
55
+
56
+ .btn {
57
+ position: relative;
58
+ padding: 1rem 2.5rem;
59
+ font-size: 1.1rem;
60
+ font-weight: 500;
61
+ border-radius: 12px;
62
+ cursor: pointer;
63
+ transition: all 0.3s ease;
64
+ overflow: hidden;
65
+ border: none;
66
+ min-width: 200px;
67
+ }
68
+
69
+ .btn::before {
70
+ content: '';
71
+ position: absolute;
72
+ top: 0;
73
+ left: 0;
74
+ width: 100%;
75
+ height: 100%;
76
+ background: rgba(255, 255, 255, 0.1);
77
+ transform: translateX(-100%);
78
+ transition: transform 0.3s ease;
79
+ }
80
+
81
+ .btn:hover::before {
82
+ transform: translateX(0);
83
+ }
84
+
85
+ .btn:hover {
86
+ transform: translateY(-2px);
87
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
88
+ }
89
+
90
+ .btn-single {
91
+ background: linear-gradient(45deg, #4f46e5, #7c3aed);
92
+ color: white;
93
+ }
94
+
95
+ .btn-multiple {
96
+ background: linear-gradient(45deg, #10b981, #059669);
97
+ color: white;
98
+ }
99
+
100
+ .animated-bg {
101
+ position: fixed;
102
+ top: 0;
103
+ left: 0;
104
+ width: 100%;
105
+ height: 100%;
106
+ z-index: 0;
107
+ overflow: hidden;
108
+ }
109
+
110
+ .floating-circle {
111
+ position: absolute;
112
+ border-radius: 50%;
113
+ opacity: 0.1;
114
+ }
115
+
116
+ .circle1 {
117
+ animation: moveCircle1 15s ease-in-out infinite;
118
+ }
119
+
120
+ .circle2 {
121
+ animation: moveCircle2 20s ease-in-out infinite;
122
+ }
123
+
124
+ .circle3 {
125
+ animation: moveCircle3 25s ease-in-out infinite;
126
+ }
127
+
128
+ @keyframes moveCircle1 {
129
+ 0% { transform: translate(-50%, -50%); }
130
+ 50% { transform: translate(-40%, -40%); }
131
+ 100% { transform: translate(-50%, -50%); }
132
+ }
133
+
134
+ @keyframes moveCircle2 {
135
+ 0% { transform: translate(-50%, -50%); }
136
+ 50% { transform: translate(-60%, -40%); }
137
+ 100% { transform: translate(-50%, -50%); }
138
+ }
139
+
140
+ @keyframes moveCircle3 {
141
+ 0% { transform: translate(-50%, -50%); }
142
+ 50% { transform: translate(-40%, -60%); }
143
+ 100% { transform: translate(-50%, -50%); }
144
+ }
145
+
146
+ .title-gradient {
147
+ background: linear-gradient(to right, #fff, #a5b4fc);
148
+ -webkit-background-clip: text;
149
+ background-clip: text;
150
+ color: transparent;
151
+ font-size: 3rem; /* Increased font size */
152
+ font-weight: 900; /* Bolder font weight */
153
+ letter-spacing: 1px;
154
+ text-align: center;
155
+ margin-bottom: 1.5rem;
156
+ }
157
+
158
+ .subtitle {
159
+ color: #94a3b8;
160
+ font-size: 1.1rem;
161
+ line-height: 1.6;
162
+ max-width: 600px;
163
+ margin: 0.5rem auto;
164
+ text-align: center;
165
+ }
166
+
167
+ @media (max-width: 768px) {
168
+ .btn-container {
169
+ flex-direction: column;
170
+ align-items: center;
171
+ gap: 1rem;
172
+ padding: 0 1rem;
173
+ }
174
+
175
+ .glass-card {
176
+ padding: 2rem;
177
+ }
178
+
179
+ .btn {
180
+ width: 100%;
181
+ max-width: 300px;
182
+ }
183
+ }
184
+ </style>
185
+ </head>
186
+ <body>
187
+ <div class="animated-bg">
188
+ <div class="floating-circle circle1" style="width: 300px; height: 300px; background: #4f46e5; left: 50%; top: 50%;"></div>
189
+ <div class="floating-circle circle2" style="width: 200px; height: 200px; background: #10b981; left: 50%; top: 50%;"></div>
190
+ <div class="floating-circle circle3" style="width: 150px; height: 150px; background: #7c3aed; left: 50%; top: 50%;"></div>
191
+ </div>
192
+
193
+ <div class="container animate_animated animate_fadeIn">
194
+ <div class="glass-card">
195
+ <h1 class="title-gradient animate_animated animate_fadeInDown">Lock Screen Classifier</h1>
196
+ <p class="subtitle animate_animated animatefadeIn animate_delay-1s">
197
+ Welcome to our <strong>PRISM</strong> Project
198
+ </p>
199
+ <p class="subtitle animate_animated animatefadeIn animate_delay-1s">
200
+ Let's classify the images with Proper Reasoning for doing so...
201
+ </p>
202
+ <div class="btn-container animate_animated animatefadeInUp animate_delay-2s">
203
+ <button class="btn btn-single" onclick="window.open('/single', '_blank')">
204
+ Single Image
205
+ </button>
206
+ <button class="btn btn-multiple" onclick="window.open('/multiple', '_blank')">
207
+ Multiple Images
208
+ </button>
209
+ </div>
210
+ </div>
211
+ </div>
212
+ </body>
213
+ </html>
templates/multiple.html ADDED
@@ -0,0 +1,1186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Multiple Classification</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/dropzone.min.css">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
+ <style>
11
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
12
+
13
+ :root {
14
+ --primary-color: #4f46e5;
15
+ --primary-dark: #4338ca;
16
+ --success-color: #22c55e;
17
+ --error-color: #ef4444;
18
+ --background-color: #1a1a2e;
19
+ --card-background: rgba(255, 255, 255, 0.1);
20
+ --text-primary: #ffffff;
21
+ --text-secondary: #a5b4fc;
22
+ --border-color: rgba(255, 255, 255, 0.18);
23
+ }
24
+
25
+ body {
26
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
27
+ min-height: 100vh;
28
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
29
+ display: flex;
30
+ justify-content: center;
31
+ align-items: center;
32
+ margin: 0;
33
+ color: var(--text-primary);
34
+ overflow-x: hidden;
35
+ }
36
+
37
+ .container {
38
+ max-width: 1200px;
39
+ margin: 0 auto;
40
+ padding: 2rem;
41
+ position: relative;
42
+ z-index: 1;
43
+ width: 100%;
44
+ }
45
+
46
+ .glass-card {
47
+ background: var(--card-background);
48
+ backdrop-filter: blur(10px);
49
+ border-radius: 20px;
50
+ padding: 2rem;
51
+ box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
52
+ border: 1px solid var(--border-color);
53
+ transform: translateY(0);
54
+ transition: transform 0.3s ease;
55
+ text-align: center;
56
+ }
57
+
58
+ .glass-card:hover {
59
+ transform: translateY(-5px);
60
+ }
61
+
62
+ .header {
63
+ text-align: center;
64
+ margin-bottom: 1.5rem;
65
+ }
66
+
67
+ .header h1 {
68
+ font-size: 2rem;
69
+ color: var(--text-primary);
70
+ margin-bottom: 0.25rem;
71
+ font-weight: 700;
72
+ }
73
+
74
+ .header p {
75
+ color: var(--text-secondary);
76
+ font-size: 1rem;
77
+ }
78
+
79
+ .upload-section {
80
+ background: var(--card-background);
81
+ border-radius: 0.75rem;
82
+ padding: 1.5rem;
83
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
84
+ margin-bottom: 1.5rem;
85
+ }
86
+
87
+ .dropzone {
88
+ border: 2px dashed var(--primary-color);
89
+ border-radius: 1rem;
90
+ background: rgba(255, 255, 255, 0.05);
91
+ padding: 2rem;
92
+ min-height: 200px;
93
+ display: flex;
94
+ flex-direction: column;
95
+ align-items: center;
96
+ justify-content: center;
97
+ transition: all 0.3s ease;
98
+ }
99
+
100
+ .dropzone:hover {
101
+ background: rgba(255, 255, 255, 0.1);
102
+ border-color: var(--primary-dark);
103
+ }
104
+
105
+ .dropzone .dz-message {
106
+ margin: 1rem 0;
107
+ text-align: center;
108
+ }
109
+
110
+ .dropzone .dz-preview {
111
+ background: var(--card-background);
112
+ border-radius: 1rem;
113
+ border: 1px solid var(--border-color);
114
+ padding: 0.5rem;
115
+ margin: 1rem;
116
+ transition: transform 0.2s ease;
117
+ }
118
+
119
+ .dropzone .dz-preview:hover {
120
+ transform: translateY(-2px);
121
+ }
122
+
123
+ .dropzone .dz-image {
124
+ width: 120px !important;
125
+ height: 120px !important;
126
+ border-radius: 0.75rem !important;
127
+ overflow: hidden;
128
+ position: relative;
129
+ }
130
+
131
+ .dropzone .dz-image img {
132
+ width: 100%;
133
+ height: 100%;
134
+ object-fit: cover;
135
+ }
136
+
137
+ .dropzone .dz-details {
138
+ padding-top: 0.5rem;
139
+ text-align: center;
140
+ }
141
+
142
+ .dropzone .dz-filename {
143
+ color: var(--text-primary);
144
+ font-size: 0.875rem;
145
+ max-width: 120px;
146
+ overflow: hidden;
147
+ text-overflow: ellipsis;
148
+ white-space: nowrap;
149
+ }
150
+
151
+ .dropzone .dz-remove {
152
+ color: var(--error-color);
153
+ text-decoration: none;
154
+ font-size: 0.875rem;
155
+ margin-top: 0.5rem;
156
+ display: inline-block;
157
+ transition: opacity 0.2s ease;
158
+ }
159
+
160
+ .dropzone .dz-remove:hover {
161
+ opacity: 0.8;
162
+ }
163
+
164
+ .dropzone .dz-preview .dz-progress {
165
+ height: 4px;
166
+ background: rgba(255, 255, 255, 0.1);
167
+ margin-top: 0.5rem;
168
+ border-radius: 2px;
169
+ }
170
+
171
+ .dropzone .dz-preview .dz-progress .dz-upload {
172
+ background: var(--primary-color);
173
+ border-radius: 2px;
174
+ }
175
+
176
+ .preview-container {
177
+ display: flex;
178
+ flex-wrap: wrap;
179
+ gap: 1rem;
180
+ margin-top: 1rem;
181
+ }
182
+
183
+ .control-panel {
184
+ display: flex;
185
+ gap: 1rem;
186
+ margin-top: 1rem;
187
+ justify-content: center;
188
+ }
189
+
190
+ .btn {
191
+ padding: 0.75rem 1.5rem;
192
+ border-radius: 0.5rem;
193
+ border: none;
194
+ color: white;
195
+ font-weight: 500;
196
+ cursor: pointer;
197
+ transition: all 0.2s ease;
198
+ display: flex;
199
+ align-items: center;
200
+ gap: 0.5rem;
201
+ font-size: 1rem;
202
+ }
203
+
204
+ .btn:hover {
205
+ transform: translateY(-1px);
206
+ }
207
+
208
+ .btn-clear {
209
+ background: var(--text-secondary);
210
+ }
211
+
212
+ .btn-classify {
213
+ background: var(--primary-color);
214
+ }
215
+
216
+ .btn-classify:hover {
217
+ background: var(--primary-dark);
218
+ }
219
+
220
+ .btn-download {
221
+ background: var(--success-color);
222
+ }
223
+
224
+ .btn-download:hover {
225
+ background: #1e9c4f;
226
+ }
227
+
228
+ .results-section {
229
+ margin-top: 1.5rem;
230
+ }
231
+
232
+ .section-header {
233
+ display: flex;
234
+ justify-content: space-between;
235
+ align-items: center;
236
+ margin-bottom: 1rem;
237
+ padding: 1rem;
238
+ border-radius: 0.5rem;
239
+ box-shadow: 0 2px 4px -1px rgb(0 0 0 / 0.1);
240
+ background: var(--card-background);
241
+ }
242
+
243
+ .section-header h2 {
244
+ margin: 0;
245
+ display: flex;
246
+ align-items: center;
247
+ gap: 0.5rem;
248
+ font-size: 1.25rem;
249
+ color: var(--text-primary);
250
+ }
251
+
252
+ .pass-header {
253
+ background: rgba(34, 197, 94, 0.1);
254
+ color: var(--success-color);
255
+ }
256
+
257
+ .fail-header {
258
+ background: rgba(239, 68, 68, 0.1);
259
+ color: var(--error-color);
260
+ }
261
+
262
+ .download-btn {
263
+ padding: 0.75rem 1.5rem;
264
+ border-radius: 0.5rem;
265
+ color: white;
266
+ text-decoration: none;
267
+ font-weight: 500;
268
+ transition: all 0.2s ease;
269
+ display: flex;
270
+ align-items: center;
271
+ gap: 0.5rem;
272
+ font-size: 1rem;
273
+ }
274
+
275
+ .download-btn:hover {
276
+ transform: translateY(-1px);
277
+ }
278
+
279
+ .download-btn.pass {
280
+ background: var(--success-color);
281
+ }
282
+
283
+ .download-btn.fail {
284
+ background: var(--error-color);
285
+ }
286
+
287
+ .image-list {
288
+ display: flex;
289
+ flex-direction: column;
290
+ gap: 0.75rem;
291
+ margin-bottom: 1.5rem;
292
+ }
293
+
294
+ .image-ribbon {
295
+ display: flex;
296
+ align-items: center;
297
+ padding: 0.75rem;
298
+ background: var(--card-background);
299
+ border-radius: 0.5rem;
300
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
301
+ transition: all 0.2s ease;
302
+ cursor: pointer;
303
+ }
304
+
305
+ .image-ribbon:hover {
306
+ transform: translateX(4px);
307
+ background: rgba(255, 255, 255, 0.1);
308
+ }
309
+
310
+ .thumbnail {
311
+ width: 50px;
312
+ height: 50px;
313
+ border-radius: 0.375rem;
314
+ object-fit: cover;
315
+ margin-right: 1rem;
316
+ }
317
+
318
+ .image-info {
319
+ flex: 1;
320
+ }
321
+
322
+ .filename {
323
+ font-weight: 500;
324
+ color: var(--text-primary);
325
+ margin-top: 0.5rem;
326
+ font-size: 1rem;
327
+ white-space: normal;
328
+ word-break: break-word;
329
+ text-align: center;
330
+ }
331
+
332
+ .labels {
333
+ display: flex;
334
+ flex-wrap: wrap;
335
+ gap: 0.5rem;
336
+ justify-content: center;
337
+ align-items: center;
338
+ margin: 0 auto;
339
+ position: relative;
340
+ transition: all 0.3s ease;
341
+ }
342
+
343
+ .label {
344
+ background: rgba(255, 255, 255, 0.1);
345
+ padding: 0.5rem 1rem;
346
+ border-radius: 9999px;
347
+ font-size: 0.875rem;
348
+ color: var(--text-secondary);
349
+ white-space: nowrap;
350
+ text-align: center;
351
+ }
352
+
353
+ #summary {
354
+ background: var(--card-background);
355
+ padding: 1rem;
356
+ border-radius: 0.5rem;
357
+ text-align: center;
358
+ font-size: 1rem;
359
+ font-weight: 500;
360
+ margin-bottom: 1.5rem;
361
+ box-shadow: 0 2px 4px -1px rgb(0 0 0 / 0.1);
362
+ color: var(--text-primary);
363
+ }
364
+
365
+ .progress-container {
366
+ margin-top: 1rem;
367
+ background: rgba(255, 255, 255, 0.1);
368
+ border-radius: 0.5rem;
369
+ overflow: hidden;
370
+ }
371
+
372
+ .progress-bar {
373
+ height: 6px;
374
+ background: var(--primary-color);
375
+ width: 0;
376
+ transition: width 0.3s ease;
377
+ }
378
+
379
+ .loading-overlay {
380
+ display: none;
381
+ position: fixed;
382
+ top: 0;
383
+ left: 0;
384
+ width: 100%;
385
+ height: 100%;
386
+ background: rgba(0, 0, 0, 0.95);
387
+ z-index: 1000;
388
+ justify-content: center;
389
+ align-items: center;
390
+ color: white;
391
+ }
392
+
393
+ .loading-content {
394
+ background: var(--card-background);
395
+ padding: 2rem;
396
+ border-radius: 1rem;
397
+ width: 90%;
398
+ max-width: 500px;
399
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
400
+ text-align: center;
401
+ color: var(--text-primary);
402
+ }
403
+
404
+ .loading-header {
405
+ text-align: center;
406
+ margin-bottom: 1.5rem;
407
+ }
408
+
409
+ .loading-header h3 {
410
+ color: var(--primary-color);
411
+ margin: 0;
412
+ margin-bottom: 0.5rem;
413
+ }
414
+
415
+ .loading-spinner {
416
+ margin: 1rem 0;
417
+ font-size: 2rem;
418
+ color: var(--primary-color);
419
+ }
420
+
421
+ .loading-progress {
422
+ font-size: 1.1rem;
423
+ color: var(--text-primary);
424
+ }
425
+
426
+ .image-modal {
427
+ display: none;
428
+ position: fixed;
429
+ top: 0;
430
+ left: 0;
431
+ width: 100%;
432
+ height: 100%;
433
+ background: rgba(0, 0, 0, 0.9);
434
+ z-index: 2000;
435
+ justify-content: center;
436
+ align-items: center;
437
+ }
438
+
439
+ .modal-content {
440
+ position: relative;
441
+ max-width: 90%;
442
+ max-height: 90vh;
443
+ margin: auto;
444
+ display: flex;
445
+ flex-direction: column;
446
+ align-items: center;
447
+ }
448
+
449
+ .modal-image {
450
+ max-width: 100%;
451
+ max-height: 80vh;
452
+ object-fit: contain;
453
+ border-radius: 0.5rem;
454
+ }
455
+
456
+ .modal-close {
457
+ position: absolute;
458
+ top: -2rem;
459
+ right: 0;
460
+ color: white;
461
+ font-size: 1.5rem;
462
+ cursor: pointer;
463
+ background: none;
464
+ border: none;
465
+ padding: 0.5rem;
466
+ }
467
+
468
+ .results-table {
469
+ width: 100%;
470
+ border-collapse: collapse;
471
+ margin-top: 1.5rem;
472
+ background: var(--card-background);
473
+ border-radius: 0.5rem;
474
+ overflow: hidden;
475
+ table-layout: fixed;
476
+ }
477
+
478
+ .results-table th,
479
+ .results-table td {
480
+ padding: 0.75rem 0.5rem;
481
+ vertical-align: middle;
482
+ text-align: center;
483
+ border-bottom: 1px solid var(--border-color);
484
+ }
485
+
486
+ .results-table .serial {
487
+ width: 80px;
488
+ }
489
+
490
+ .results-table .image-col {
491
+ width: 200px;
492
+ }
493
+
494
+ .results-table .result-col {
495
+ width: 180px;
496
+ }
497
+
498
+ .results-table .labels-col {
499
+ width: auto;
500
+ min-width: 150px;
501
+ padding-left: 1rem;
502
+ text-align: center !important;
503
+ }
504
+
505
+ .results-table td {
506
+ padding: 0.75rem 0.5rem;
507
+ vertical-align: middle;
508
+ border-bottom: 1px solid var(--border-color);
509
+ text-align: left;
510
+ }
511
+
512
+ .results-table td.serial,
513
+ .results-table td.image-col,
514
+ .results-table td.result-col {
515
+ text-align: center;
516
+ }
517
+
518
+ .results-table th.labels-col {
519
+ text-align: center;
520
+ }
521
+
522
+ .results-table td.labels-col {
523
+ text-align: center !important;
524
+ padding: 1rem;
525
+ }
526
+
527
+ .results-table tbody tr:last-child td {
528
+ border-bottom: none;
529
+ }
530
+
531
+ .result-pass,
532
+ .result-fail {
533
+ display: inline-block;
534
+ padding: 0.25rem 0.75rem;
535
+ border-radius: 9999px;
536
+ text-align: center;
537
+ }
538
+
539
+ .result-pass {
540
+ background: rgba(34, 197, 94, 0.1);
541
+ }
542
+
543
+ .result-fail {
544
+ background: rgba(239, 68, 68, 0.1);
545
+ }
546
+
547
+ .dropzone {
548
+ border: 2px dashed var(--primary-color);
549
+ border-radius: 0.5rem;
550
+ background: rgba(255, 255, 255, 0.1);
551
+ padding: 1rem;
552
+ min-height: 150px;
553
+ }
554
+
555
+ .dropzone .dz-preview {
556
+ margin: 1rem;
557
+ }
558
+
559
+ .dropzone .dz-preview .dz-image {
560
+ border-radius: 0.5rem;
561
+ }
562
+
563
+ .dropzone .dz-preview .dz-details {
564
+ color: var(--text-primary);
565
+ }
566
+
567
+ .dropzone .dz-preview {
568
+ display: inline-block;
569
+ margin: 1rem;
570
+ position: relative;
571
+ }
572
+
573
+ .dropzone .dz-preview:nth-child(n+5) {
574
+ opacity: 0.3;
575
+ }
576
+
577
+ .dropzone .dz-preview:nth-child(n+6) {
578
+ display: none !important;
579
+ }
580
+
581
+ .dropzone .dz-preview.dz-file-preview .dz-image,
582
+ .dropzone .dz-preview .dz-image {
583
+ border-radius: 8px;
584
+ width: 150px;
585
+ height: 150px;
586
+ }
587
+
588
+ .dz-more-indicator {
589
+ position: absolute;
590
+ top: 0;
591
+ left: 0;
592
+ width: 100%;
593
+ height: 100%;
594
+ background: rgba(0, 0, 0, 0.7);
595
+ color: white;
596
+ display: flex;
597
+ justify-content: center;
598
+ align-items: center;
599
+ font-size: 1.2rem;
600
+ border-radius: 8px;
601
+ display: none;
602
+ }
603
+
604
+ .dropzone .dz-preview:nth-child(5) .dz-more-indicator {
605
+ display: flex;
606
+ }
607
+
608
+ .dropzone .dz-preview {
609
+ display: none !important;
610
+ }
611
+
612
+ .upload-section {
613
+ background: var(--card-background);
614
+ border-radius: 1rem;
615
+ padding: 2rem;
616
+ margin-bottom: 2rem;
617
+ }
618
+
619
+ .dropzone {
620
+ border: 2px dashed var(--primary-color);
621
+ border-radius: 1rem;
622
+ padding: 2rem;
623
+ text-align: center;
624
+ background: rgba(255, 255, 255, 0.05);
625
+ transition: all 0.3s ease;
626
+ min-height: 200px;
627
+ display: flex;
628
+ align-items: center;
629
+ justify-content: center;
630
+ }
631
+
632
+ .dropzone .dz-message {
633
+ margin: 0;
634
+ }
635
+
636
+ .dropzone:hover {
637
+ background: rgba(255, 255, 255, 0.08);
638
+ border-color: var(--primary-dark);
639
+ }
640
+
641
+ #thumbnail-preview-box {
642
+ display: none;
643
+ position: absolute;
644
+ z-index: 9999;
645
+ background: var(--card-background);
646
+ border: 1px solid var(--border-color);
647
+ border-radius: 0.75rem;
648
+ padding: 1rem;
649
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
650
+ max-width: 300px;
651
+ }
652
+
653
+ #thumbnail-preview-box img {
654
+ max-width: 100%;
655
+ border-radius: 0.5rem;
656
+ display: block;
657
+ }
658
+ </style>
659
+ </head>
660
+ <body>
661
+ <div class="container">
662
+ <div class="glass-card">
663
+ <div class="header">
664
+ <h1>Multiple Images Classification</h1>
665
+ <p>Classify and segregate multiple images at once...</p>
666
+ </div>
667
+
668
+ <div class="upload-section">
669
+ <form action="/upload_multiple" class="dropzone" id="upload-form">
670
+ <div class="dz-message">
671
+ <i class="fas fa-cloud-upload-alt fa-3x" style="color: var(--primary-color); margin-bottom: 1rem;"></i>
672
+ <h3 style="margin: 0.5rem 0; color: var(--text-primary);">Drop files here</h3>
673
+ <p style="color: var(--text-secondary); margin: 0;">or click to browse</p>
674
+ <div id="file-count" style="margin-top: 1rem; color: var(--text-secondary);"></div>
675
+ </div>
676
+ </form>
677
+ <div class="progress-container">
678
+ <div class="progress-bar" id="upload-progress"></div>
679
+ </div>
680
+ <div class="control-panel">
681
+ <button class="btn btn-clear" onclick="clearAllImages()">
682
+ <i class="fas fa-trash-alt"></i>
683
+ Clear All
684
+ </button>
685
+ <button class="btn btn-classify" id="classify-btn" onclick="highlightClassification()">
686
+ <i class="fas fa-tags"></i>
687
+ Classify
688
+ </button>
689
+ <!-- New Download HTML Button -->
690
+ <button class="btn btn-download" id="download-html-btn" onclick="downloadPageAsHTML()">
691
+ <i class="fas fa-download"></i>
692
+ Download HTML
693
+ </button>
694
+ </div>
695
+ </div>
696
+
697
+ <div id="summary"></div>
698
+
699
+ <div class="results-section">
700
+ <table class="results-table" id="results-table">
701
+ <thead>
702
+ <tr>
703
+ <th class="serial">S.No</th>
704
+ <th class="image-col">Image</th>
705
+ <th class="result-col">Result</th>
706
+ <th class="labels-col">Labels</th>
707
+ </tr>
708
+ </thead>
709
+ <tbody id="results-tbody">
710
+ </tbody>
711
+ </table>
712
+ </div>
713
+
714
+ <!-- Loading Overlay -->
715
+ <div class="loading-overlay" id="loading-overlay">
716
+ <div class="loading-content">
717
+ <div class="loading-header">
718
+ <h3>Classifying Images</h3>
719
+ <p>Please wait while we process your images...</p>
720
+ </div>
721
+ <div class="loading-spinner">
722
+ <i class="fas fa-spinner fa-spin"></i>
723
+ </div>
724
+ <div class="loading-files" id="loading-files">
725
+ <!-- Loading file items will be added here dynamically -->
726
+ </div>
727
+ </div>
728
+ </div>
729
+
730
+ <!-- Image Modal -->
731
+ <div class="image-modal" id="image-modal">
732
+ <div class="modal-content">
733
+ <button class="modal-close" onclick="closeModal()">
734
+ <i class="fas fa-times"></i>
735
+ </button>
736
+ <img class="modal-image" id="modal-image" src="" alt="">
737
+ </div>
738
+ </div>
739
+ </div>
740
+ </div>
741
+
742
+ <div id="thumbnail-preview-box">
743
+ <img id="thumbnail-preview-img" src="" alt="">
744
+ </div>
745
+
746
+ <script>
747
+ let isClassifying = false; // Track if classification is in progress
748
+
749
+ Dropzone.options.uploadForm = {
750
+ paramName: "file",
751
+ maxFilesize: 16,
752
+ acceptedFiles: "image/*",
753
+ previewsContainer: false,
754
+ init: function() {
755
+ let myDropzone = this;
756
+
757
+ // Add event handler for when files are dropped or selected
758
+ this.on("addedfile", function(file) {
759
+ // If this is the first file in a new batch
760
+ if (myDropzone.files.length === 1) {
761
+ // Clear previous results
762
+ document.getElementById('results-tbody').innerHTML = '';
763
+ document.getElementById('summary').innerHTML = '';
764
+ document.getElementById('upload-progress').style.width = '0';
765
+
766
+ // Clear server-side uploads
767
+ fetch('/clear_uploads', { method: 'POST' })
768
+ .then(response => response.json())
769
+ .catch(error => console.error('Error:', error));
770
+ }
771
+ updateFileCount(myDropzone.files.length);
772
+ });
773
+
774
+ this.on("success", function(file, response) {
775
+ updateFileCount(this.files.length);
776
+ });
777
+
778
+ this.on("removedfile", function() {
779
+ updateFileCount(this.files.length);
780
+ });
781
+
782
+ this.on("totaluploadprogress", function(progress) {
783
+ document.getElementById('upload-progress').style.width = progress + "%";
784
+ });
785
+
786
+ // Add event handler for when the dropzone is clicked
787
+ this.element.querySelector(".dz-message").addEventListener("click", function() {
788
+ // Clear everything before opening file dialog
789
+ myDropzone.removeAllFiles(true);
790
+ document.getElementById('results-tbody').innerHTML = '';
791
+ document.getElementById('summary').innerHTML = '';
792
+ document.getElementById('upload-progress').style.width = '0';
793
+
794
+ // Clear server-side uploads
795
+ fetch('/clear_uploads', { method: 'POST' })
796
+ .then(response => response.json())
797
+ .catch(error => console.error('Error:', error));
798
+ });
799
+ }
800
+ };
801
+
802
+ function updateFileCount(count) {
803
+ const fileCount = document.getElementById('file-count');
804
+ if (count > 0) {
805
+ fileCount.textContent = `${count} file${count === 1 ? '' : 's'} selected`;
806
+ } else {
807
+ fileCount.textContent = '';
808
+ }
809
+ }
810
+
811
+ function updateSummary() {
812
+ const passCount = document.querySelectorAll('.result-pass').length;
813
+ const failCount = document.querySelectorAll('.result-fail').length;
814
+ const totalFiles = passCount + failCount;
815
+
816
+ document.getElementById('summary').innerHTML = `
817
+ <i class="fas fa-chart-pie"></i> Results:
818
+ <span style="color: var(--success-color)">${passCount} passed</span>,
819
+ <span style="color: var(--error-color)">${failCount} failed</span>
820
+ out of ${totalFiles} total images
821
+ `;
822
+ }
823
+
824
+ function clearAllImages() {
825
+ fetch('/clear_uploads', { method: 'POST' })
826
+ .then(response => response.json())
827
+ .then(data => {
828
+ // Clear table
829
+ document.getElementById('results-tbody').innerHTML = '';
830
+ document.getElementById('summary').innerHTML = '';
831
+ document.getElementById('upload-progress').style.width = '0';
832
+
833
+ // Clear dropzone
834
+ let myDropzone = Dropzone.forElement("#upload-form");
835
+ myDropzone.removeAllFiles(true);
836
+
837
+ // Remove any remaining previews manually
838
+ document.querySelectorAll('.dz-preview').forEach(el => el.remove());
839
+ })
840
+ .catch(error => console.error('Error:', error));
841
+ }
842
+
843
+ function createTableRow(result, index) {
844
+ const labels = result.status === 'Fail' ? result.labels : [];
845
+
846
+ return `
847
+ <tr>
848
+ <td class="serial">${index + 1}</td>
849
+ <td class="image-col">
850
+ <img src="data:image/jpeg;base64,${result.image}"
851
+ alt="${result.filename}"
852
+ class="thumbnail"
853
+ onclick="openModal(this)"
854
+ style="cursor: pointer;">
855
+ <div class="filename">${result.filename}</div>
856
+ </td>
857
+ <td class="result-col">
858
+ <span class="result-${result.status.toLowerCase()}">${result.status}</span>
859
+ </td>
860
+ <td class="labels-col">
861
+ ${result.status === 'Fail' ? `
862
+ <div class="labels">
863
+ ${labels.map(label => `
864
+ <span class="label">${label}</span>
865
+ `).join('')}
866
+ </div>
867
+ ` : '-'}
868
+ </td>
869
+ </tr>
870
+ `;
871
+ }
872
+
873
+ function openModal(imageElement) {
874
+ const modal = document.getElementById('image-modal');
875
+ const modalImage = document.getElementById('modal-image');
876
+
877
+ // Set the modal image source to the clicked thumbnail's source
878
+ modalImage.src = imageElement.src;
879
+ modal.style.display = 'flex'; // Show the modal
880
+
881
+ // Add event listener to close the modal when clicking outside the image
882
+ document.addEventListener('click', closeModalOnClickOutside);
883
+ }
884
+
885
+ function closeModal() {
886
+ const modal = document.getElementById('image-modal');
887
+ modal.style.display = 'none'; // Hide the modal
888
+
889
+ // Remove the event listener for closing the modal
890
+ document.removeEventListener('click', closeModalOnClickOutside);
891
+ }
892
+
893
+ function closeModalOnClickOutside(event) {
894
+ const modal = document.getElementById('image-modal');
895
+ const modalContent = document.querySelector('.modal-content');
896
+
897
+ // Check if the click is outside the modal content
898
+ if (!modalContent.contains(event.target)) {
899
+ closeModal();
900
+ }
901
+ }
902
+
903
+ function handleEscKey(event) {
904
+ if (event.key === 'Escape') {
905
+ closeModal();
906
+ }
907
+ }
908
+
909
+ // Add event listener for Escape key
910
+ document.addEventListener('keydown', handleEscKey);
911
+
912
+ async function processNextImage(filesProcessed, totalFiles) {
913
+ try {
914
+ const response = await fetch('/classify_multiple', { method: 'POST' });
915
+ const result = await response.json();
916
+
917
+ if (result.error) {
918
+ throw new Error(result.error);
919
+ }
920
+
921
+ if (result.filename) {
922
+ // Update the corresponding row
923
+ const row = document.getElementById(`result-row-${filesProcessed}`);
924
+ if (row) {
925
+ row.outerHTML = createTableRow(result, filesProcessed);
926
+ updateSummary();
927
+ }
928
+
929
+ // Process next image if more remain
930
+ if (filesProcessed + 1 < totalFiles) {
931
+ setTimeout(() => {
932
+ processNextImage(filesProcessed + 1, totalFiles);
933
+ }, 100);
934
+ } else {
935
+ finishClassification();
936
+ }
937
+ } else {
938
+ finishClassification();
939
+ }
940
+ } catch (error) {
941
+ console.error('Error processing image:', error);
942
+ showError(filesProcessed);
943
+ finishClassification();
944
+ }
945
+ }
946
+
947
+ function finishClassification() {
948
+ isClassifying = false;
949
+ const classifyBtn = document.getElementById('classify-btn');
950
+ classifyBtn.disabled = false;
951
+ classifyBtn.innerHTML = '<i class="fas fa-tags"></i> Classify';
952
+ }
953
+
954
+ async function highlightClassification() {
955
+ if (isClassifying) {
956
+ alert('Classification in progress...');
957
+ return;
958
+ }
959
+
960
+ const resultsBody = document.getElementById('results-tbody');
961
+ const myDropzone = Dropzone.forElement("#upload-form");
962
+
963
+ if (!myDropzone.files.length) {
964
+ alert("Please upload some images first!");
965
+ return;
966
+ }
967
+
968
+ // Reset classification state and UI
969
+ isClassifying = true;
970
+ const classifyBtn = document.getElementById('classify-btn');
971
+ classifyBtn.disabled = true;
972
+ classifyBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Classifying...';
973
+
974
+ // Clear previous results
975
+ resultsBody.innerHTML = '';
976
+
977
+ // Reset server-side state
978
+ try {
979
+ await fetch('/clear_uploads', { method: 'POST' });
980
+
981
+ // Re-upload all files
982
+ for (const file of myDropzone.files) {
983
+ const formData = new FormData();
984
+ formData.append('file', file);
985
+ await fetch('/upload_multiple', {
986
+ method: 'POST',
987
+ body: formData
988
+ });
989
+ }
990
+
991
+ // Create placeholder rows
992
+ myDropzone.files.forEach((file, index) => {
993
+ resultsBody.insertAdjacentHTML('beforeend', `
994
+ <tr id="result-row-${index}">
995
+ <td class="serial">${index + 1}</td>
996
+ <td class="image-col">
997
+ <div class="filename">${file.name}</div>
998
+ <div style="color: var(--text-secondary);">Waiting...</div>
999
+ </td>
1000
+ <td class="result-col">
1001
+ <i class="fas fa-clock"></i>
1002
+ </td>
1003
+ <td class="labels-col">
1004
+ <i class="fas fa-clock"></i>
1005
+ </td>
1006
+ </tr>
1007
+ `);
1008
+ });
1009
+
1010
+ // Start processing
1011
+ processNextImage(0, myDropzone.files.length);
1012
+
1013
+ } catch (error) {
1014
+ console.error('Error during classification:', error);
1015
+ alert('Error during classification. Please try again.');
1016
+ finishClassification();
1017
+ }
1018
+ }
1019
+
1020
+ function showError(index) {
1021
+ const row = document.getElementById(`result-row-${index}`);
1022
+ if (row) {
1023
+ row.innerHTML = `
1024
+ <td class="serial">${index + 1}</td>
1025
+ <td colspan="3" style="color: var(--error-color);">
1026
+ Error processing image. Please try again.
1027
+ </td>
1028
+ `;
1029
+ }
1030
+ }
1031
+
1032
+ function toggleLabels(labelsDiv) {
1033
+ labelsDiv.classList.toggle('expanded');
1034
+ }
1035
+
1036
+ document.getElementById('image-modal').addEventListener('click', function(e) {
1037
+ if (e.target === this) {
1038
+ closeModal();
1039
+ }
1040
+ });
1041
+
1042
+ document.addEventListener('click', function(e) {
1043
+ if (e.target.classList.contains('thumbnail')) {
1044
+ e.preventDefault();
1045
+ const box = document.getElementById('thumbnail-preview-box');
1046
+ const img = document.getElementById('thumbnail-preview-img');
1047
+ img.src = e.target.src;
1048
+ box.style.display = 'block';
1049
+ box.style.left = (e.pageX + 10) + 'px';
1050
+ box.style.top = (e.pageY + 10) + 'px';
1051
+
1052
+ document.addEventListener('keydown', hideOnEscape);
1053
+ } else if (!e.target.closest('#thumbnail-preview-box')) {
1054
+ hidePreview();
1055
+ }
1056
+ });
1057
+
1058
+ function hideOnEscape(e) {
1059
+ if (e.key === 'Escape') {
1060
+ hidePreview();
1061
+ }
1062
+ }
1063
+
1064
+ function hidePreview() {
1065
+ const box = document.getElementById('thumbnail-preview-box');
1066
+ box.style.display = 'none';
1067
+ document.removeEventListener('keydown', hideOnEscape);
1068
+ }
1069
+
1070
+ // Function to download the entire page as HTML
1071
+ function downloadPageAsHTML() {
1072
+ const now = new Date();
1073
+ const date = now.toLocaleDateString('en-CA');
1074
+ const time = now.toLocaleTimeString('en-GB')
1075
+ .replace(/:/g, '-')
1076
+ .split('.')[0];
1077
+
1078
+ // Show progress message
1079
+ const btn = document.getElementById('download-html-btn');
1080
+ const originalBtnText = btn.innerHTML;
1081
+ btn.disabled = true;
1082
+ btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Preparing report...';
1083
+
1084
+ try {
1085
+ // Get summary content once
1086
+ const summaryContent = document.getElementById('summary').innerHTML;
1087
+
1088
+ // Collect only failed images
1089
+ const rows = document.querySelectorAll('#results-tbody tr');
1090
+ if (!rows.length) {
1091
+ throw new Error('No data to save');
1092
+ }
1093
+
1094
+ const tableData = Array.from(rows)
1095
+ .filter(row => row.querySelector('.result-col span').textContent === 'Fail')
1096
+ .map(row => ({
1097
+ serialNo: row.querySelector('.serial').textContent,
1098
+ image: row.querySelector('.thumbnail').src,
1099
+ filename: row.querySelector('.filename').textContent,
1100
+ status: row.querySelector('.result-col span').textContent,
1101
+ labels: row.querySelector('.labels') ?
1102
+ Array.from(row.querySelector('.labels').querySelectorAll('.label'))
1103
+ .map(label => label.textContent) : []
1104
+ }));
1105
+
1106
+ if (tableData.length === 0) {
1107
+ alert('No failed images to save!');
1108
+ return;
1109
+ }
1110
+
1111
+ // Split data into smaller chunks (5 rows per chunk)
1112
+ const CHUNK_SIZE = 5;
1113
+ const chunks = [];
1114
+ for (let i = 0; i < tableData.length; i += CHUNK_SIZE) {
1115
+ chunks.push(tableData.slice(i, i + CHUNK_SIZE));
1116
+ }
1117
+
1118
+ // Progress tracking
1119
+ let currentChunk = 0;
1120
+ const totalChunks = chunks.length;
1121
+
1122
+ async function sendChunk() {
1123
+ try {
1124
+ // Update button text with progress
1125
+ btn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> Saving ${currentChunk + 1}/${totalChunks}`;
1126
+
1127
+ const response = await fetch('/save_report_chunk', {
1128
+ method: 'POST',
1129
+ headers: {
1130
+ 'Content-Type': 'application/json'
1131
+ },
1132
+ body: JSON.stringify({
1133
+ chunkNumber: currentChunk,
1134
+ totalChunks: totalChunks,
1135
+ date: date,
1136
+ time: time,
1137
+ summary: currentChunk === 0 ? summaryContent : '',
1138
+ chunk: chunks[currentChunk]
1139
+ })
1140
+ });
1141
+
1142
+ if (!response.ok) {
1143
+ const error = await response.json();
1144
+ throw new Error(error.error || 'Failed to save report');
1145
+ }
1146
+
1147
+ const result = await response.json();
1148
+
1149
+ if (result.error) {
1150
+ throw new Error(result.error);
1151
+ }
1152
+
1153
+ if (currentChunk < totalChunks - 1) {
1154
+ currentChunk++;
1155
+ await sendChunk();
1156
+ } else {
1157
+ alert(`Report saved successfully in Reports/${date}/Report_${date}_${time}.html`);
1158
+ btn.innerHTML = originalBtnText;
1159
+ btn.disabled = false;
1160
+ }
1161
+ } catch (error) {
1162
+ console.error('Error:', error);
1163
+ alert('Error saving report: ' + error.message);
1164
+ btn.innerHTML = originalBtnText;
1165
+ btn.disabled = false;
1166
+ }
1167
+ }
1168
+
1169
+ // Start sending chunks
1170
+ sendChunk().catch(error => {
1171
+ console.error('Error in chunk processing:', error);
1172
+ alert('Error saving report: ' + error.message);
1173
+ btn.innerHTML = originalBtnText;
1174
+ btn.disabled = false;
1175
+ });
1176
+
1177
+ } catch (error) {
1178
+ console.error('Error:', error);
1179
+ alert('Error preparing report: ' + error.message);
1180
+ btn.innerHTML = originalBtnText;
1181
+ btn.disabled = false;
1182
+ }
1183
+ }
1184
+ </script>
1185
+ </body>
1186
+ </html>
templates/report_template.html ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Classification Report</title>
7
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/min/dropzone.min.js"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.9.3/dropzone.min.css">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
+ <style>
11
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
12
+
13
+ :root {
14
+ --primary-color: #4f46e5;
15
+ --primary-dark: #4338ca;
16
+ --success-color: #22c55e;
17
+ --error-color: #ef4444;
18
+ --background-color: #1a1a2e;
19
+ --card-background: rgba(255, 255, 255, 0.1);
20
+ --text-primary: #ffffff;
21
+ --text-secondary: #a5b4fc;
22
+ --border-color: rgba(255, 255, 255, 0.18);
23
+ }
24
+
25
+ body {
26
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
27
+ min-height: 100vh;
28
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
29
+ margin: 0;
30
+ padding: 2rem;
31
+ color: var(--text-primary);
32
+ }
33
+
34
+ .container {
35
+ max-width: 1200px;
36
+ margin: 0 auto;
37
+ }
38
+
39
+ .glass-card {
40
+ background: var(--card-background);
41
+ backdrop-filter: blur(10px);
42
+ border-radius: 20px;
43
+ padding: 2rem;
44
+ box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
45
+ border: 1px solid var(--border-color);
46
+ }
47
+
48
+ .header {
49
+ text-align: center;
50
+ margin-bottom: 2rem;
51
+ }
52
+
53
+ .header h1 {
54
+ font-size: 2rem;
55
+ margin-bottom: 0.5rem;
56
+ }
57
+
58
+ #summary {
59
+ background: var(--card-background);
60
+ padding: 1rem;
61
+ border-radius: 0.5rem;
62
+ text-align: center;
63
+ margin-bottom: 1.5rem;
64
+ backdrop-filter: blur(5px);
65
+ }
66
+
67
+ .results-table {
68
+ width: 100%;
69
+ border-collapse: collapse;
70
+ background: var(--card-background);
71
+ border-radius: 0.5rem;
72
+ overflow: hidden;
73
+ }
74
+
75
+ .results-table th,
76
+ .results-table td {
77
+ padding: 1rem;
78
+ text-align: center;
79
+ border: 1px solid var(--border-color);
80
+ }
81
+
82
+ .serial {
83
+ width: 80px;
84
+ }
85
+
86
+ .image-col {
87
+ width: 200px;
88
+ }
89
+
90
+ .result-col {
91
+ width: 180px;
92
+ }
93
+
94
+ .labels-col {
95
+ width: auto;
96
+ min-width: 150px;
97
+ }
98
+
99
+ .thumbnail {
100
+ width: 100px;
101
+ height: 100px;
102
+ object-fit: cover;
103
+ border-radius: 0.5rem;
104
+ cursor: pointer;
105
+ }
106
+
107
+ .result-pass,
108
+ .result-fail {
109
+ display: inline-block;
110
+ padding: 0.5rem 1rem;
111
+ border-radius: 9999px;
112
+ text-align: center;
113
+ }
114
+
115
+ .result-pass {
116
+ background: rgba(34, 197, 94, 0.1);
117
+ color: var(--success-color);
118
+ }
119
+
120
+ .result-fail {
121
+ background: rgba(239, 68, 68, 0.1);
122
+ color: var(--error-color);
123
+ }
124
+
125
+ .labels {
126
+ display: flex;
127
+ flex-wrap: wrap;
128
+ gap: 0.5rem;
129
+ justify-content: center;
130
+ }
131
+
132
+ .label {
133
+ display: inline-block;
134
+ background: rgba(255, 255, 255, 0.1);
135
+ padding: 0.5rem 1rem;
136
+ border-radius: 9999px;
137
+ margin: 0.25rem;
138
+ color: var(--text-secondary);
139
+ }
140
+
141
+ .image-modal {
142
+ display: none;
143
+ position: fixed;
144
+ top: 0;
145
+ left: 0;
146
+ width: 100%;
147
+ height: 100%;
148
+ background: rgba(0, 0, 0, 0.9);
149
+ z-index: 1000;
150
+ justify-content: center;
151
+ align-items: center;
152
+ }
153
+
154
+ .modal-content {
155
+ position: relative;
156
+ max-width: 90%;
157
+ max-height: 90vh;
158
+ }
159
+
160
+ .modal-image {
161
+ max-width: 100%;
162
+ max-height: 90vh;
163
+ object-fit: contain;
164
+ border-radius: 0.5rem;
165
+ }
166
+
167
+ .modal-close {
168
+ position: absolute;
169
+ top: -2rem;
170
+ right: 0;
171
+ color: white;
172
+ font-size: 2rem;
173
+ cursor: pointer;
174
+ background: none;
175
+ border: none;
176
+ padding: 0.5rem;
177
+ }
178
+
179
+ #thumbnail-preview-box {
180
+ display: none;
181
+ position: absolute;
182
+ z-index: 9999;
183
+ background: var(--card-background);
184
+ border: 1px solid var(--border-color);
185
+ border-radius: 0.75rem;
186
+ padding: 1rem;
187
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
188
+ max-width: 300px;
189
+ }
190
+
191
+ #thumbnail-preview-box img {
192
+ max-width: 100%;
193
+ border-radius: 0.5rem;
194
+ display: block;
195
+ }
196
+
197
+ .filename {
198
+ margin-top: 0.5rem;
199
+ font-size: 0.9rem;
200
+ word-break: break-word;
201
+ }
202
+ </style>
203
+ </head>
204
+ <body>
205
+ <div class="container">
206
+ <div class="glass-card">
207
+ <div class="header">
208
+ <h1>Failed Images Report</h1>
209
+ <p>Date: {date} | Time: {time}</p>
210
+ </div>
211
+
212
+ <div id="summary">
213
+ {summary_content}
214
+ </div>
215
+
216
+ <div class="results-section">
217
+ <table class="results-table" id="results-table">
218
+ <thead>
219
+ <tr>
220
+ <th class="serial">S.No</th>
221
+ <th class="image-col">Image</th>
222
+ <th class="result-col">Result</th>
223
+ <th class="labels-col">Labels</th>
224
+ </tr>
225
+ </thead>
226
+ <tbody>
227
+ {table_rows}
228
+ </tbody>
229
+ </table>
230
+ </div>
231
+
232
+ <!-- Image Modal -->
233
+ <div class="image-modal" id="image-modal">
234
+ <div class="modal-content">
235
+ <button class="modal-close">
236
+ <i class="fas fa-times"></i>
237
+ </button>
238
+ <img class="modal-image" id="modal-image" src="" alt="">
239
+ </div>
240
+ </div>
241
+ </div>
242
+ </div>
243
+
244
+ <div id="thumbnail-preview-box">
245
+ <img id="thumbnail-preview-img" src="" alt="">
246
+ </div>
247
+
248
+ <script>
249
+ document.addEventListener('DOMContentLoaded', function() {
250
+ // Modal functionality
251
+ const modal = document.getElementById('image-modal');
252
+ const modalImg = modal.querySelector('.modal-image');
253
+ const closeBtn = modal.querySelector('.modal-close');
254
+
255
+ // Close modal events
256
+ closeBtn.onclick = () => modal.style.display = 'none';
257
+ modal.onclick = e => {
258
+ if (e.target === modal) modal.style.display = 'none';
259
+ };
260
+
261
+ // Escape key to close modal
262
+ document.addEventListener('keydown', e => {
263
+ if (e.key === 'Escape') modal.style.display = 'none';
264
+ });
265
+
266
+ // Thumbnail preview functionality
267
+ document.addEventListener('click', function(e) {
268
+ if (e.target.classList.contains('thumbnail')) {
269
+ e.preventDefault();
270
+ const box = document.getElementById('thumbnail-preview-box');
271
+ const img = document.getElementById('thumbnail-preview-img');
272
+ img.src = e.target.src;
273
+ box.style.display = 'block';
274
+ box.style.left = (e.pageX + 10) + 'px';
275
+ box.style.top = (e.pageY + 10) + 'px';
276
+ } else if (!e.target.closest('#thumbnail-preview-box')) {
277
+ document.getElementById('thumbnail-preview-box').style.display = 'none';
278
+ }
279
+ });
280
+ });
281
+ </script>
282
+ </body>
283
+ </html>
templates/single.html ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Single Classification</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
8
+ <style>
9
+ html.dark {
10
+ --bg-color: #1a202c;
11
+ --text-color: #e2e8f0;
12
+ --card-bg-color: #2d3748;
13
+ --card-border-color: #4a5568;
14
+ --btn-bg-color: #4a5568;
15
+ --btn-hover-bg-color: #2c5282;
16
+ --btn-text-color: #e2e8f0;
17
+ }
18
+
19
+ html.light {
20
+ --bg-color: #f7fafc;
21
+ --text-color: #2d3748;
22
+ --card-bg-color: #ffffff;
23
+ --card-border-color: #e2e8f0;
24
+ --btn-bg-color: #3182ce;
25
+ --btn-hover-bg-color: #2c5282;
26
+ --btn-text-color: #ffffff;
27
+ }
28
+
29
+ body {
30
+ background-color: var(--bg-color);
31
+ color: var(--text-color);
32
+ }
33
+
34
+ .card {
35
+ background-color: var(--card-bg-color);
36
+ border: 1px solid var(--card-border-color);
37
+ }
38
+
39
+ .btn {
40
+ background-color: var(--btn-bg-color);
41
+ color: var(--btn-text-color);
42
+ }
43
+
44
+ .btn:hover {
45
+ background-color: var(--btn-hover-bg-color);
46
+ }
47
+
48
+ .loading {
49
+ display: none;
50
+ position: fixed;
51
+ top: 0;
52
+ left: 0;
53
+ width: 100%;
54
+ height: 100%;
55
+ background: rgba(0, 0, 0, 0.5);
56
+ z-index: 1000;
57
+ }
58
+
59
+ .result-table {
60
+ font-family: monospace;
61
+ white-space: pre;
62
+ }
63
+
64
+ .classification-text {
65
+ font-size: 1.5rem;
66
+ font-weight: bold;
67
+ }
68
+
69
+ .btn-classify {
70
+ background-color: #10b981;
71
+ color: white;
72
+ font-size: 1.1rem;
73
+ padding: 0.75rem 1.5rem;
74
+ border-radius: 9999px;
75
+ }
76
+
77
+ .btn-classify:hover {
78
+ background-color: #059669;
79
+ }
80
+
81
+ .btn-remove {
82
+ background-color: #ef4444;
83
+ color: white;
84
+ font-size: 1.1rem;
85
+ padding: 0.75rem 1.5rem;
86
+ border-radius: 9999px;
87
+ }
88
+
89
+ .btn-remove:hover {
90
+ background-color: #dc2626;
91
+ }
92
+
93
+ .results-container {
94
+ display: flex;
95
+ flex-direction: column;
96
+ align-items: center;
97
+ text-align: center;
98
+ }
99
+
100
+ .classification-result {
101
+ margin: 1.5rem 0;
102
+ text-align: center;
103
+ }
104
+
105
+ .table-container {
106
+ width: 100%;
107
+ display: flex;
108
+ justify-content: center;
109
+ text-align: left;
110
+ }
111
+
112
+ /* Loading overlay styles */
113
+ .loading-overlay {
114
+ background: rgba(0, 0, 0, 0.7);
115
+ color: white;
116
+ }
117
+
118
+ .loading-text {
119
+ color: white;
120
+ font-size: 1.2rem;
121
+ font-weight: 500;
122
+ }
123
+ </style>
124
+ </head>
125
+ <body class="min-h-screen">
126
+ <div class="container mx-auto px-4 py-8">
127
+ <div class="flex justify-end mb-4">
128
+ <button id="themeToggle" class="bg-gray-800 text-white px-4 py-2 rounded hover:bg-gray-600">
129
+ ☀️
130
+ </button>
131
+ </div>
132
+ <h1 class="text-3xl font-bold text-center mb-8">Single Image Classification</h1>
133
+
134
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-8">
135
+ <!-- Left Column: Image Upload -->
136
+ <div class="card p-6 rounded-lg shadow-md">
137
+ <div id="uploadSection" class="mb-4">
138
+ <label class="block text-sm font-bold mb-2">Upload Image</label>
139
+ <input type="file" id="imageInput" accept=".jpg,.jpeg,.png" class="hidden">
140
+ <button onclick="document.getElementById('imageInput').click()" class="btn w-full py-2 px-4 rounded">
141
+ Select Image
142
+ </button>
143
+ </div>
144
+
145
+ <div id="imagePreview" class="mt-4 hidden">
146
+ <img id="preview" class="max-w-full h-auto rounded-lg">
147
+ <div class="mt-4 flex space-x-4">
148
+ <button onclick="removeImage()" class="btn btn-remove flex-1">
149
+ Remove
150
+ </button>
151
+ <button onclick="classifyImage()" class="btn btn-classify flex-1">
152
+ Classify
153
+ </button>
154
+ </div>
155
+ </div>
156
+ </div>
157
+
158
+ <!-- Right Column: Results -->
159
+ <div class="card p-6 rounded-lg shadow-md">
160
+ <h2 class="text-xl font-bold mb-4 text-center">Classification Results</h2>
161
+ <div id="results" class="results-container"></div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+
166
+ <!-- Loading Overlay -->
167
+ <div id="loading" class="loading flex items-center justify-center">
168
+ <div class="loading-overlay p-8 rounded-lg text-center">
169
+ <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-white mx-auto"></div>
170
+ <p id="loadingText" class="mt-4 loading-text">Processing...</p>
171
+ </div>
172
+ </div>
173
+
174
+ <script>
175
+ // Theme handling
176
+ const themeToggle = document.getElementById('themeToggle');
177
+ const htmlElement = document.documentElement;
178
+
179
+ // Initialize theme
180
+ if (!localStorage.getItem('theme')) {
181
+ localStorage.setItem('theme', 'dark');
182
+ }
183
+
184
+ // Apply saved theme or default to dark
185
+ const currentTheme = localStorage.getItem('theme') || 'dark';
186
+ htmlElement.classList.remove('light', 'dark');
187
+ htmlElement.classList.add(currentTheme);
188
+ themeToggle.textContent = currentTheme === 'dark' ? '☀️' : '🌙';
189
+
190
+ // Theme toggle handler
191
+ themeToggle.addEventListener('click', () => {
192
+ const isDark = htmlElement.classList.contains('dark');
193
+ htmlElement.classList.remove('dark', 'light');
194
+ const newTheme = isDark ? 'light' : 'dark';
195
+ htmlElement.classList.add(newTheme);
196
+ localStorage.setItem('theme', newTheme);
197
+ themeToggle.textContent = newTheme === 'dark' ? '☀️' : '🌙';
198
+ });
199
+
200
+ // File handling
201
+ let currentFile = null;
202
+
203
+ document.getElementById('imageInput').addEventListener('change', function(e) {
204
+ const file = e.target.files[0];
205
+ if (file) {
206
+ uploadFile(file);
207
+ }
208
+ });
209
+
210
+ function uploadFile(file) {
211
+ const formData = new FormData();
212
+ formData.append('file', file);
213
+
214
+ showLoading('Uploading...');
215
+
216
+ fetch('/upload_single', {
217
+ method: 'POST',
218
+ body: formData
219
+ })
220
+ .then(response => response.json())
221
+ .then(data => {
222
+ if (data.filename) {
223
+ currentFile = data.filename;
224
+ document.getElementById('preview').src = `/static/uploads/single/${data.filename}`;
225
+ document.getElementById('imagePreview').classList.remove('hidden');
226
+ document.getElementById('uploadSection').classList.add('hidden');
227
+ } else {
228
+ alert(data.error || 'Upload failed');
229
+ }
230
+ })
231
+ .catch(error => {
232
+ console.error('Error:', error);
233
+ alert('Upload failed');
234
+ })
235
+ .finally(() => {
236
+ hideLoading();
237
+ });
238
+ }
239
+
240
+ function removeImage() {
241
+ document.getElementById('imageInput').value = '';
242
+ document.getElementById('imagePreview').classList.add('hidden');
243
+ document.getElementById('uploadSection').classList.remove('hidden');
244
+ document.getElementById('results').innerHTML = '';
245
+ currentFile = null;
246
+ }
247
+
248
+ function classifyImage() {
249
+ if (!currentFile) {
250
+ alert('Please upload an image first');
251
+ return;
252
+ }
253
+
254
+ showLoading('Classifying...');
255
+
256
+ fetch('/classify_single', {
257
+ method: 'POST',
258
+ headers: {
259
+ 'Content-Type': 'application/json',
260
+ },
261
+ body: JSON.stringify({ filename: currentFile })
262
+ })
263
+ .then(response => response.json())
264
+ .then(data => {
265
+ if (data.error) {
266
+ throw new Error(data.error);
267
+ }
268
+ const resultsDiv = document.getElementById('results');
269
+ resultsDiv.innerHTML = `
270
+ <div class="classification-result">
271
+ <span class="font-bold">Classification: </span>
272
+ <span class="classification-text ${data.classification === 'Pass' ? 'text-green-600' : 'text-red-600'}">
273
+ ${data.classification}
274
+ </span>
275
+ </div>
276
+ <div class="table-container">
277
+ <pre class="whitespace-pre-wrap font-mono text-sm">${data.result_table}</pre>
278
+ </div>
279
+ `;
280
+ })
281
+ .catch(error => {
282
+ console.error('Error:', error);
283
+ alert('Classification failed: ' + error.message);
284
+ })
285
+ .finally(() => {
286
+ hideLoading();
287
+ });
288
+ }
289
+
290
+ function showLoading(text) {
291
+ document.getElementById('loading').style.display = 'flex';
292
+ document.getElementById('loadingText').textContent = text;
293
+ }
294
+
295
+ function hideLoading() {
296
+ document.getElementById('loading').style.display = 'none';
297
+ }
298
+ </script>
299
+ </body>
300
+ </html>
utils.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import numpy as np
3
+ import cv2
4
+ from PIL import Image
5
+ import random
6
+ import torch
7
+ import torchvision.transforms as T
8
+ from torchvision.transforms.functional import InterpolationMode
9
+ from difflib import SequenceMatcher
10
+ from nltk.metrics.distance import edit_distance
11
+ import nltk
12
+
13
+ # Ensure NLTK data is downloaded
14
+ try:
15
+ nltk.data.find('corpora/words.zip')
16
+ except LookupError:
17
+ nltk.download('words')
18
+ try:
19
+ nltk.data.find('tokenizers/punkt')
20
+ except LookupError:
21
+ nltk.download('punkt')
22
+
23
+ from nltk.corpus import words
24
+
25
+ def set_seed(seed=42):
26
+ random.seed(seed)
27
+ np.random.seed(seed)
28
+ torch.manual_seed(seed)
29
+ # torch.cuda.manual_seed_all(seed) # Uncomment if using GPU
30
+ torch.backends.cudnn.deterministic = True
31
+ torch.backends.cudnn.benchmark = False
32
+
33
+ def build_transform(input_size=448):
34
+ mean = (0.485, 0.456, 0.406)
35
+ std = (0.229, 0.224, 0.225)
36
+ return T.Compose([
37
+ T.Lambda(lambda img: img.convert('RGB')),
38
+ T.Resize((input_size, input_size), interpolation=InterpolationMode.BICUBIC),
39
+ T.ToTensor(),
40
+ T.Normalize(mean=mean, std=std)
41
+ ])
42
+
43
+ def get_roi(image_path_or_obj, *roi):
44
+ """
45
+ Extracts ROI from an image path or PIL Image object.
46
+ """
47
+ if isinstance(image_path_or_obj, str):
48
+ image = Image.open(image_path_or_obj).convert('RGB')
49
+ else:
50
+ image = image_path_or_obj.convert('RGB')
51
+
52
+ width, height = image.size
53
+
54
+ roi_x_start = int(width * roi[0])
55
+ roi_y_start = int(height * roi[1])
56
+ roi_x_end = int(width * roi[2])
57
+ roi_y_end = int(height * roi[3])
58
+
59
+ cropped_image = image.crop((roi_x_start, roi_y_start, roi_x_end, roi_y_end))
60
+ return cropped_image
61
+
62
+ def clean_text(text):
63
+ return re.sub(r'[^a-zA-Z0-9]', '', text).strip().lower()
64
+
65
+ def are_strings_similar(str1, str2, max_distance=3, max_length_diff=2):
66
+ if str1 == str2:
67
+ return True
68
+ if abs(len(str1) - len(str2)) > max_length_diff:
69
+ return False
70
+ edit_distance_value = edit_distance(str1, str2)
71
+ return edit_distance_value <= max_distance
72
+
73
+ def blur_image(image, strength):
74
+ image_np = np.array(image)
75
+ blur_strength = int(strength * 50)
76
+ blur_strength = max(1, blur_strength | 1)
77
+ blurred_image = cv2.GaussianBlur(image_np, (blur_strength, blur_strength), 0)
78
+ blurred_pil_image = Image.fromarray(blurred_image)
79
+ return blurred_pil_image
80
+
81
+ def is_blank(text, limit=15):
82
+ return len(text) < limit
83
+
84
+ def string_similarity(a, b):
85
+ return SequenceMatcher(None, a.lower(), b.lower()).ratio()
86
+
87
+ def find_similar_substring(text, keyword, threshold=0.9):
88
+ text = text.lower()
89
+ keyword = keyword.lower()
90
+
91
+ if keyword in text:
92
+ return True
93
+
94
+ keyword_length = len(keyword.split())
95
+ words_list = text.split()
96
+
97
+ for i in range(len(words_list) - keyword_length + 1):
98
+ phrase = ' '.join(words_list[i:i + keyword_length])
99
+ similarity = string_similarity(phrase, keyword)
100
+ if similarity >= threshold:
101
+ return True
102
+
103
+ return False
104
+
105
+ def destroy_text_roi(image, *roi_params):
106
+ image_np = np.array(image)
107
+
108
+ h, w, _ = image_np.shape
109
+ x1 = int(roi_params[0] * w)
110
+ y1 = int(roi_params[1] * h)
111
+ x2 = int(roi_params[2] * w)
112
+ y2 = int(roi_params[3] * h)
113
+
114
+ roi = image_np[y1:y2, x1:x2]
115
+
116
+ blurred_roi = cv2.GaussianBlur(roi, (75, 75), 0)
117
+ noise = np.random.randint(0, 50, (blurred_roi.shape[0], blurred_roi.shape[1], 3), dtype=np.uint8)
118
+ noisy_blurred_roi = cv2.add(blurred_roi, noise)
119
+ image_np[y1:y2, x1:x2] = noisy_blurred_roi
120
+ return Image.fromarray(image_np)
121
+
122
+ def is_english(text):
123
+ allowed_pattern = re.compile(
124
+ r'^[a-zA-Z०-९\u0930\s\.,!?\-;:"\'()]*$'
125
+ )
126
+ return bool(allowed_pattern.match(text))
127
+
128
+ def is_valid_english(text):
129
+ english_words = set(words.words())
130
+ cleaned_words = ''.join(c.lower() if c.isalnum() else ' ' for c in text).split()
131
+ return all(word.lower() in english_words for word in cleaned_words)