Upload 20 files
Browse files- Dockerfile +29 -0
- README.md +27 -0
- app.py +458 -0
- config.py +109 -0
- model_handler.py +149 -0
- modules/__init__.py +0 -0
- modules/__pycache__/__init__.cpython-311.pyc +0 -0
- modules/__pycache__/content_checks.cpython-311.pyc +0 -0
- modules/__pycache__/text_checks.cpython-311.pyc +0 -0
- modules/__pycache__/visual_checks.cpython-311.pyc +0 -0
- modules/content_checks.py +79 -0
- modules/text_checks.py +125 -0
- modules/visual_checks.py +99 -0
- pipeline.py +112 -0
- requirements.txt +16 -0
- templates/index.html +213 -0
- templates/multiple.html +1186 -0
- templates/report_template.html +283 -0
- templates/single.html +300 -0
- utils.py +131 -0
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)
|