devranx commited on
Commit
d790e98
·
0 Parent(s):

Initial deploy with LFS images and audio

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .agent/workflows/deploy_to_huggingface.md +73 -0
  2. .gitattributes +5 -0
  3. .gitignore +44 -0
  4. Dockerfile +38 -0
  5. app.py +299 -0
  6. backend/__init__.py +0 -0
  7. backend/config.py +109 -0
  8. backend/model_handler.py +157 -0
  9. backend/modules/__init__.py +0 -0
  10. backend/modules/content_checks.py +79 -0
  11. backend/modules/text_checks.py +125 -0
  12. backend/modules/visual_checks.py +99 -0
  13. backend/pipeline.py +151 -0
  14. backend/utils.py +131 -0
  15. deployment_guide.md +104 -0
  16. frontend/App.tsx +128 -0
  17. frontend/components/BackgroundAnimation.tsx +103 -0
  18. frontend/components/BatchAnalysis.tsx +389 -0
  19. frontend/components/Hero.tsx +113 -0
  20. frontend/components/Icons.tsx +64 -0
  21. frontend/components/SingleAnalysis.tsx +253 -0
  22. frontend/index.html +190 -0
  23. frontend/index.tsx +15 -0
  24. frontend/package-lock.json +2441 -0
  25. frontend/package.json +23 -0
  26. frontend/public/favicon.png +3 -0
  27. frontend/services/apiService.ts +141 -0
  28. frontend/services/geminiService.ts +96 -0
  29. frontend/tsconfig.json +29 -0
  30. frontend/types.ts +43 -0
  31. frontend/vite.config.ts +28 -0
  32. requirements.txt +17 -0
  33. static/certificates/excellence.png +3 -0
  34. static/certificates/participation.png +3 -0
  35. static/logo.png +3 -0
  36. static/samples/1.png +3 -0
  37. static/samples/Angry husband trying to kill his wife indoors_ Concept of domestic violence.jpg +3 -0
  38. static/samples/Custom Bloodied Scream Buck 120 Knife Prop (1).jpg +3 -0
  39. static/samples/NoTag2.jpg +3 -0
  40. static/samples/Picture4.png +3 -0
  41. static/samples/Screenshot_20241020_142718_One UI Home.jpg +3 -0
  42. static/samples/Screenshot_20241021-142831_One UI Home.jpg +3 -0
  43. static/samples/Screenshot_20241022-125250_One UI Home.jpg +3 -0
  44. static/samples/Screenshot_20241022-133424_One UI Home.jpg +3 -0
  45. static/samples/conf2.jpg +3 -0
  46. static/samples/natraj.jpg +3 -0
  47. static/samples/notEng.jpeg +3 -0
  48. static/samples/pixelcut-export (1).jpeg +3 -0
  49. static/samples/pixelcut-export (2).png +3 -0
  50. static/samples/pixelcut-export (3).jpeg +3 -0
.agent/workflows/deploy_to_huggingface.md ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ description: Deploy to Hugging Face Spaces
3
+ ---
4
+
5
+ # Deploying to Hugging Face Spaces
6
+
7
+ This guide explains how to deploy the Samsung Prism Prototype to a Hugging Face Space and keep it synced with your local changes.
8
+
9
+ ## Prerequisites
10
+
11
+ 1. A Hugging Face account.
12
+ 2. Git installed on your machine.
13
+
14
+ ## Step 1: Create a New Space
15
+
16
+ 1. Go to [huggingface.co/spaces](https://huggingface.co/spaces).
17
+ 2. Click **"Create new Space"**.
18
+ 3. **Name**: `samsung-prism-prototype` (or similar).
19
+ 4. **License**: MIT (optional).
20
+ 5. **SDK**: Select **Docker** (Recommended for custom dependencies like OpenCV/EasyOCR) or **Gradio** (if you were using Gradio, but we are using Flask, so Docker is best).
21
+ * *Note*: Since we are using Flask, **Docker** is the most flexible option. Select **Docker** -> **Blank**.
22
+ 6. Click **"Create Space"**.
23
+
24
+ ## Step 2: Prepare Your Project for Docker
25
+
26
+ Ensure you have a `Dockerfile` in the root of your project.
27
+ (I will verify/create this for you in the next steps).
28
+
29
+ ## Step 3: Connect Local Repo to Hugging Face
30
+
31
+ 1. Initialize git if you haven't already:
32
+ ```bash
33
+ git init
34
+ ```
35
+ 2. Add the Hugging Face remote (replace `YOUR_USERNAME` and `SPACE_NAME`):
36
+ ```bash
37
+ git remote add space https://huggingface.co/spaces/YOUR_USERNAME/SPACE_NAME
38
+ ```
39
+ 3. Pull the initial files (like README.md) from the Space:
40
+ ```bash
41
+ git pull space main --allow-unrelated-histories
42
+ ```
43
+
44
+ ## Step 4: Push Changes
45
+
46
+ Whenever you make changes locally, run these commands to update the Space:
47
+
48
+ ```bash
49
+ # Add all changes
50
+ git add .
51
+
52
+ # Commit changes
53
+ git commit -m "Update prototype"
54
+
55
+ # Push to Hugging Face
56
+ git push space main
57
+ ```
58
+
59
+ ## Step 5: Handling Large Files (Models)
60
+
61
+ **IMPORTANT**: Hugging Face Spaces have a hard limit on file sizes for Git (LFS).
62
+ Since your `Models/` directory is large, you should **NOT** push it to Git directly if the files are huge.
63
+ Instead, rely on the `model_handler.py` logic to download models from the Hugging Face Hub at runtime, or use `git lfs` if you must upload custom weights.
64
+
65
+ Since we updated `model_handler.py` to download from HF Hub if local files are missing, you can simply **exclude** the `Models/` directory from git.
66
+
67
+ Create/Update `.gitignore`:
68
+ ```
69
+ Models/
70
+ __pycache__/
71
+ .venv/
72
+ .env
73
+ ```
.gitattributes ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ *.mp3 filter=lfs diff=lfs merge=lfs -text
2
+ *.png filter=lfs diff=lfs merge=lfs -text
3
+ *.jpg filter=lfs diff=lfs merge=lfs -text
4
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
5
+ *.gif filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
25
+ # Python
26
+ __pycache__/
27
+ *.py[cod]
28
+ *$py.class
29
+ .venv/
30
+ .env
31
+
32
+ # Models (Large files)
33
+ Models/
34
+
35
+
36
+ # Temporary/Testing
37
+ Testing ROIs for Ribbon/
38
+
39
+ # User Data
40
+ static/uploads/
41
+ Reports/
42
+
43
+ # Build Artifacts (Built by Docker)
44
+ static/react/
Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Stage 1: Build React Frontend
2
+ FROM node:18-alpine as builder
3
+ WORKDIR /app
4
+ COPY frontend/ ./frontend/
5
+ WORKDIR /app/frontend
6
+ RUN npm install
7
+ # Create the output directory structure expected by vite.config.ts
8
+ RUN mkdir -p ../static/react
9
+ RUN npm run build
10
+
11
+ # Stage 2: Python Backend
12
+ FROM python:3.10-slim
13
+
14
+ # Install system dependencies for OpenCV and others
15
+ RUN apt-get update && apt-get install -y \
16
+ libgl1 \
17
+ libglib2.0-0 \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ WORKDIR /app
21
+
22
+ # Copy requirements and install dependencies
23
+ COPY requirements.txt .
24
+ RUN pip install --no-cache-dir -r requirements.txt
25
+
26
+ # Copy backend code
27
+ COPY . .
28
+
29
+ # Copy built frontend from builder stage
30
+ # Note: Vite build output was configured to ../static/react, so it's at /app/static/react in builder
31
+ COPY --from=builder /app/static/react ./static/react
32
+
33
+ # Expose the port
34
+ # Expose the port (Hugging Face Spaces uses 7860)
35
+ EXPOSE 7860
36
+
37
+ # Command to run the application
38
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from flask import Flask, render_template, request, jsonify, send_file, Response, stream_with_context
3
+ from werkzeug.utils import secure_filename
4
+ import os
5
+ from pathlib import Path
6
+ import shutil
7
+ import io
8
+ import json
9
+ import logging
10
+ from backend.pipeline import classify
11
+
12
+ # Configure logging
13
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
14
+ logger = logging.getLogger(__name__)
15
+
16
+ app = Flask(__name__)
17
+
18
+ # Configuration
19
+ ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
20
+ UPLOAD_FOLDER_SINGLE = 'static/uploads/single'
21
+ UPLOAD_FOLDER_MULTIPLE = 'static/uploads/multiple'
22
+
23
+ app.config['UPLOAD_FOLDER_SINGLE'] = UPLOAD_FOLDER_SINGLE
24
+ app.config['UPLOAD_FOLDER_MULTIPLE'] = UPLOAD_FOLDER_MULTIPLE
25
+ app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
26
+ app.config['MAX_CONTENT_LENGTH_REPORT'] = 500 * 1024 * 1024
27
+
28
+ # Ensure upload directories exist
29
+ os.makedirs(UPLOAD_FOLDER_SINGLE, exist_ok=True)
30
+ os.makedirs(UPLOAD_FOLDER_MULTIPLE, exist_ok=True)
31
+
32
+ def allowed_file(filename):
33
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
34
+
35
+ def clear_uploads(folder):
36
+ """Helper function to clear upload directories."""
37
+ if os.path.exists(folder):
38
+ for filename in os.listdir(folder):
39
+ file_path = os.path.join(folder, filename)
40
+ try:
41
+ if os.path.isfile(file_path) or os.path.islink(file_path):
42
+ os.unlink(file_path)
43
+ elif os.path.isdir(file_path):
44
+ shutil.rmtree(file_path)
45
+ except Exception as e:
46
+ logger.error(f'Failed to delete {file_path}. Reason: {e}')
47
+
48
+ @app.route('/', defaults={'path': ''})
49
+ @app.route('/<path:path>')
50
+ def index(path):
51
+ if path.startswith('static/'):
52
+ return send_file(path)
53
+ return send_file('static/react/index.html')
54
+
55
+ @app.route('/upload_single', methods=['POST'])
56
+ def upload_single():
57
+ if 'file' not in request.files:
58
+ return jsonify({'error': 'No file uploaded'}), 400
59
+ file = request.files['file']
60
+ if file.filename == '':
61
+ return jsonify({'error': 'No file selected'}), 400
62
+ if file and allowed_file(file.filename):
63
+ filename = secure_filename(file.filename)
64
+ filepath = os.path.join(app.config['UPLOAD_FOLDER_SINGLE'], filename)
65
+ file.save(filepath)
66
+ return jsonify({'filename': filename})
67
+ return jsonify({'error': 'Invalid file type'}), 400
68
+
69
+ @app.route('/classify_single', methods=['POST'])
70
+ def classify_single():
71
+ data = request.get_json()
72
+ filename = data.get('filename')
73
+ if not filename:
74
+ return jsonify({'error': 'No filename provided'}), 400
75
+ filepath = os.path.join(app.config['UPLOAD_FOLDER_SINGLE'], filename)
76
+ if not os.path.exists(filepath):
77
+ return jsonify({'error': 'File not found'}), 404
78
+
79
+ try:
80
+ classification_result, detailed_results, failure_labels = classify(filepath)
81
+ return jsonify({
82
+ 'classification': classification_result,
83
+ 'detailed_results': detailed_results
84
+ })
85
+ except Exception as e:
86
+ logger.error(f"Error in classify_single: {e}")
87
+ return jsonify({'error': str(e)}), 500
88
+
89
+ @app.route('/upload_multiple', methods=['POST'])
90
+ def upload_multiple():
91
+ logger.info("=== UPLOAD_MULTIPLE CALLED ===")
92
+ if 'file' not in request.files:
93
+ logger.warning("No 'file' in request.files")
94
+ return jsonify({'error': 'No file uploaded'}), 400
95
+
96
+ files = request.files.getlist('file')
97
+ logger.info(f"Received {len(files)} files in request")
98
+ if not files:
99
+ return jsonify({'error': 'No files selected'}), 400
100
+
101
+ try:
102
+ # Ensure temp directory exists (DON'T wipe it)
103
+ temp_dir = os.path.join(app.config['UPLOAD_FOLDER_MULTIPLE'], 'temp')
104
+ os.makedirs(temp_dir, exist_ok=True)
105
+
106
+ saved_count = 0
107
+ filename_map = {}
108
+
109
+ # Load existing map if present
110
+ map_path = os.path.join(temp_dir, 'filename_map.json')
111
+ if os.path.exists(map_path):
112
+ try:
113
+ with open(map_path, 'r') as f:
114
+ filename_map = json.load(f)
115
+ logger.info(f"Loaded existing filename map with {len(filename_map)} entries")
116
+ except Exception as e:
117
+ logger.error(f"Error loading existing filename map: {e}")
118
+
119
+ for file in files:
120
+ if file and allowed_file(file.filename):
121
+ original_filename = file.filename
122
+ filename = secure_filename(file.filename)
123
+ filepath = os.path.join(temp_dir, filename)
124
+ file.save(filepath)
125
+ filename_map[filename] = original_filename
126
+ saved_count += 1
127
+ logger.info(f"Saved: '{original_filename}' -> '{filename}'")
128
+
129
+ # Save updated filename map
130
+ with open(map_path, 'w') as f:
131
+ json.dump(filename_map, f)
132
+
133
+ # Count actual files in directory
134
+ actual_files = [f for f in os.listdir(temp_dir) if allowed_file(f)]
135
+ logger.info(f"Total files in temp directory after upload: {len(actual_files)}")
136
+ logger.info(f"Files: {actual_files}")
137
+
138
+ return jsonify({
139
+ 'message': f'Successfully uploaded {saved_count} files',
140
+ 'count': saved_count,
141
+ 'status': 'Ready'
142
+ })
143
+
144
+ except Exception as e:
145
+ logger.error(f"Error in upload_multiple: {e}", exc_info=True)
146
+ return jsonify({'error': str(e)}), 500
147
+
148
+ @app.route('/classify_multiple', methods=['POST'])
149
+ def classify_multiple():
150
+ logger.info("=== CLASSIFY_MULTIPLE CALLED ===")
151
+ def generate():
152
+ temp_dir = os.path.join(app.config['UPLOAD_FOLDER_MULTIPLE'], 'temp')
153
+ if not os.path.exists(temp_dir):
154
+ logger.warning("Temp directory does not exist")
155
+ yield json.dumps({'error': 'No files to classify'}) + '\n'
156
+ return
157
+
158
+ # Load filename map
159
+ filename_map = {}
160
+ map_path = os.path.join(temp_dir, 'filename_map.json')
161
+ if os.path.exists(map_path):
162
+ try:
163
+ with open(map_path, 'r') as f:
164
+ filename_map = json.load(f)
165
+ logger.info(f"Loaded filename map: {filename_map}")
166
+ except Exception as e:
167
+ logger.error(f"Error loading filename map: {e}")
168
+
169
+ files = [f for f in os.listdir(temp_dir) if allowed_file(f)]
170
+ logger.info(f"Processing {len(files)} files from temp directory")
171
+ logger.info(f"Files to process: {files}")
172
+
173
+ for filename in files:
174
+ filepath = os.path.join(temp_dir, filename)
175
+ logger.info(f"Classifying: {filename}")
176
+ try:
177
+ classification_result, _, failure_labels = classify(filepath)
178
+ logger.info(f"Result for {filename}: {classification_result} with labels: {failure_labels}")
179
+
180
+ # Move file
181
+ dest_dir = os.path.join(app.config['UPLOAD_FOLDER_MULTIPLE'], classification_result.lower())
182
+ os.makedirs(dest_dir, exist_ok=True)
183
+ dest_path = os.path.join(dest_dir, filename)
184
+ shutil.move(filepath, dest_path)
185
+
186
+ # Get original filename if available
187
+ original_filename = filename_map.get(filename, filename)
188
+ logger.info(f"Sending result for original filename: {original_filename}")
189
+
190
+ result = {
191
+ 'filename': original_filename,
192
+ 'status': 'pass' if classification_result == 'Pass' else 'fail',
193
+ 'labels': failure_labels,
194
+ 'score': 0
195
+ }
196
+ yield json.dumps(result) + '\n'
197
+
198
+ except Exception as e:
199
+ logger.error(f"Error processing {filename}: {e}", exc_info=True)
200
+ # Use original filename for error reporting
201
+ original_filename = filename_map.get(filename, filename)
202
+ yield json.dumps({'filename': original_filename, 'status': 'error', 'error': str(e)}) + '\n'
203
+
204
+ return Response(stream_with_context(generate()), mimetype='application/x-ndjson')
205
+
206
+ @app.route('/clear_uploads', methods=['POST'])
207
+ def clear_uploads_route():
208
+ logger.info("=== CLEAR_UPLOADS CALLED ===")
209
+ try:
210
+ clear_uploads(app.config['UPLOAD_FOLDER_SINGLE'])
211
+ clear_uploads(app.config['UPLOAD_FOLDER_MULTIPLE'])
212
+ return jsonify({'success': True})
213
+ except Exception as e:
214
+ logger.error(f"Error in clear_uploads_route: {e}")
215
+ return jsonify({'error': str(e)}), 500
216
+
217
+ @app.route('/api/use_sample', methods=['POST'])
218
+ def use_sample():
219
+ try:
220
+ data = request.get_json()
221
+ filename = data.get('filename')
222
+ destination = data.get('destination') # 'single' or 'multiple'
223
+
224
+ if not filename or not destination:
225
+ return jsonify({'error': 'Missing filename or destination'}), 400
226
+
227
+ # Validate filename (security)
228
+ if not allowed_file(filename):
229
+ return jsonify({'error': 'Invalid filename'}), 400
230
+
231
+ # Source path
232
+ src_path = os.path.join('static', 'samples', filename)
233
+ if not os.path.exists(src_path):
234
+ return jsonify({'error': 'Sample not found'}), 404
235
+
236
+ # Destination path
237
+ if destination == 'single':
238
+ dest_folder = app.config['UPLOAD_FOLDER_SINGLE']
239
+ elif destination == 'multiple':
240
+ dest_folder = os.path.join(app.config['UPLOAD_FOLDER_MULTIPLE'], 'temp')
241
+ os.makedirs(dest_folder, exist_ok=True)
242
+ else:
243
+ return jsonify({'error': 'Invalid destination'}), 400
244
+
245
+ dest_path = os.path.join(dest_folder, filename)
246
+
247
+ # Copy file
248
+ shutil.copy2(src_path, dest_path)
249
+
250
+ # For multiple, we need to update the filename map
251
+ if destination == 'multiple':
252
+ map_path = os.path.join(dest_folder, 'filename_map.json')
253
+ filename_map = {}
254
+ if os.path.exists(map_path):
255
+ try:
256
+ with open(map_path, 'r') as f:
257
+ filename_map = json.load(f)
258
+ except:
259
+ pass
260
+
261
+ filename_map[filename] = filename # Map to itself for samples
262
+
263
+ with open(map_path, 'w') as f:
264
+ json.dump(filename_map, f)
265
+
266
+ return jsonify({'success': True, 'filename': filename})
267
+
268
+ except Exception as e:
269
+ logger.error(f"Error in use_sample: {e}")
270
+ return jsonify({'error': str(e)}), 500
271
+
272
+ @app.route('/api/samples', methods=['GET'])
273
+ def get_samples():
274
+ try:
275
+ samples_dir = os.path.join('static', 'samples')
276
+ if not os.path.exists(samples_dir):
277
+ return jsonify([])
278
+
279
+ files = [f for f in os.listdir(samples_dir) if allowed_file(f)]
280
+ # Sort files for consistent order
281
+ files.sort()
282
+
283
+ samples = []
284
+ for i, filename in enumerate(files):
285
+ samples.append({
286
+ 'id': i + 1,
287
+ 'url': f'/static/samples/{filename}',
288
+ 'filename': filename
289
+ })
290
+
291
+ return jsonify(samples)
292
+ except Exception as e:
293
+ logger.error(f"Error in get_samples: {e}")
294
+ return jsonify({'error': str(e)}), 500
295
+
296
+ if __name__ == '__main__':
297
+ logger.info("SERVER STARTING ON PORT 7860")
298
+ # Disable reloader to prevent loading models twice (saves memory)
299
+ app.run(debug=False, use_reloader=False, host='0.0.0.0', port=7860)
backend/__init__.py ADDED
File without changes
backend/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
backend/model_handler.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 backend.utils import build_transform
9
+
10
+ class ModelHandler:
11
+ def __init__(self):
12
+ try:
13
+ self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
14
+ print(f"Using device: {self.device}", flush=True)
15
+ self.transform = build_transform()
16
+ self.load_models()
17
+ except Exception as e:
18
+ print(f"CRITICAL ERROR in ModelHandler.__init__: {e}", flush=True)
19
+ import traceback
20
+ traceback.print_exc()
21
+
22
+ def load_models(self):
23
+ # MODEL 1: InternVL
24
+ try:
25
+ # Check if local path exists, otherwise use HF Hub ID
26
+ local_path = os.path.join("Models", "InternVL2_5-1B-MPO")
27
+ if os.path.exists(local_path):
28
+ internvl_model_path = local_path
29
+ print(f"Loading InternVL from local path: {internvl_model_path}", flush=True)
30
+ else:
31
+ internvl_model_path = "OpenGVLab/InternVL2_5-1B-MPO" # HF Hub ID
32
+ print(f"Local model not found. Downloading InternVL from HF Hub: {internvl_model_path}", flush=True)
33
+
34
+ self.model_int = AutoModel.from_pretrained(
35
+ internvl_model_path,
36
+ torch_dtype=torch.bfloat16,
37
+ low_cpu_mem_usage=True,
38
+ trust_remote_code=True
39
+ ).eval()
40
+
41
+ for module in self.model_int.modules():
42
+ if isinstance(module, torch.nn.Dropout):
43
+ module.p = 0
44
+
45
+ self.tokenizer_int = AutoTokenizer.from_pretrained(internvl_model_path, trust_remote_code=True)
46
+ print("\nInternVL model and tokenizer loaded successfully.", flush=True)
47
+ except Exception as e:
48
+ print(f"\nError loading InternVL model or tokenizer: {e}", flush=True)
49
+ import traceback
50
+ traceback.print_exc()
51
+ self.model_int = None
52
+ self.tokenizer_int = None
53
+
54
+ # MODEL 2: EasyOCR
55
+ try:
56
+ # EasyOCR automatically handles downloading if not present
57
+ self.reader = easyocr.Reader(['en', 'hi'], gpu=False)
58
+ print("\nEasyOCR reader initialized successfully.")
59
+ except Exception as e:
60
+ print(f"\nError initializing EasyOCR reader: {e}")
61
+ self.reader = None
62
+
63
+ # MODEL 3: CLIP
64
+ try:
65
+ local_path = os.path.join("Models", "clip-vit-base-patch32")
66
+ if os.path.exists(local_path):
67
+ clip_model_path = local_path
68
+ print(f"Loading CLIP from local path: {clip_model_path}")
69
+ else:
70
+ clip_model_path = "openai/clip-vit-base-patch32" # HF Hub ID
71
+ print(f"Local model not found. Downloading CLIP from HF Hub: {clip_model_path}")
72
+
73
+ self.processor_clip = AutoProcessor.from_pretrained(clip_model_path)
74
+ self.model_clip = AutoModelForZeroShotImageClassification.from_pretrained(clip_model_path).to(self.device)
75
+ print("\nCLIP model and processor loaded successfully.")
76
+ except Exception as e:
77
+ print(f"\nError loading CLIP model or processor: {e}")
78
+ self.model_clip = None
79
+ self.processor_clip = None
80
+
81
+ def easyocr_ocr(self, image):
82
+ if not self.reader:
83
+ return ""
84
+ image_np = np.array(image)
85
+ results = self.reader.readtext(image_np, detail=1)
86
+
87
+ del image_np
88
+ gc.collect()
89
+
90
+ if not results:
91
+ return ""
92
+
93
+ sorted_results = sorted(results, key=lambda x: (x[0][0][1], x[0][0][0]))
94
+ ordered_text = " ".join([res[1] for res in sorted_results]).strip()
95
+ return ordered_text
96
+
97
+ def intern(self, image, prompt, max_tokens):
98
+ if not self.model_int or not self.tokenizer_int:
99
+ return ""
100
+
101
+ pixel_values = self.transform(image).unsqueeze(0).to(self.device).to(torch.bfloat16)
102
+ with torch.no_grad():
103
+ response, _ = self.model_int.chat(
104
+ self.tokenizer_int,
105
+ pixel_values,
106
+ prompt,
107
+ generation_config={
108
+ "max_new_tokens": max_tokens,
109
+ "do_sample": False,
110
+ "num_beams": 1,
111
+ "temperature": 1.0,
112
+ "top_p": 1.0,
113
+ "repetition_penalty": 1.0,
114
+ "length_penalty": 1.0,
115
+ "pad_token_id": self.tokenizer_int.pad_token_id
116
+ },
117
+ history=None,
118
+ return_history=True
119
+ )
120
+
121
+ del pixel_values
122
+ gc.collect()
123
+ return response
124
+
125
+ def clip(self, image, labels):
126
+ if not self.model_clip or not self.processor_clip:
127
+ return None
128
+
129
+ processed = self.processor_clip(
130
+ text=labels,
131
+ images=image,
132
+ padding=True,
133
+ return_tensors="pt"
134
+ ).to(self.device)
135
+
136
+ del image, labels
137
+ gc.collect()
138
+ return processed
139
+
140
+ def get_clip_probs(self, image, labels):
141
+ inputs = self.clip(image, labels)
142
+ if inputs is None:
143
+ return None
144
+
145
+ with torch.no_grad():
146
+ outputs = self.model_clip(**inputs)
147
+
148
+ logits_per_image = outputs.logits_per_image
149
+ probs = F.softmax(logits_per_image, dim=1)
150
+
151
+ del inputs, outputs, logits_per_image
152
+ gc.collect()
153
+
154
+ return probs
155
+
156
+ # Create a global instance to be used by modules
157
+ model_handler = ModelHandler()
backend/modules/__init__.py ADDED
File without changes
backend/modules/content_checks.py ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import torch
3
+ from PIL import Image
4
+ from backend import config
5
+ from backend.utils import find_similar_substring, destroy_text_roi
6
+ from backend.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
backend/modules/text_checks.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import emoji
3
+ from PIL import Image
4
+ from backend import config
5
+ from backend.utils import get_roi, clean_text, are_strings_similar, blur_image, is_blank, is_english, is_valid_english, destroy_text_roi
6
+ from backend.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}
backend/modules/visual_checks.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ from PIL import Image
4
+ from backend import config
5
+ from backend.utils import get_roi
6
+ from backend.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}
backend/pipeline.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from backend.modules import visual_checks, text_checks, content_checks
2
+ import logging
3
+ import random
4
+ import time
5
+
6
+ # Configure logging
7
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
8
+ logger = logging.getLogger(__name__)
9
+
10
+ # ==========================================
11
+ # REAL CLASSIFICATION LOGIC
12
+ # ==========================================
13
+ def classify_real(image_path):
14
+ """Perform complete classification with detailed results using AI models."""
15
+ # Components to check
16
+ components = [
17
+ visual_checks.image_quality,
18
+ visual_checks.ribbon,
19
+ text_checks.tagline,
20
+ text_checks.tooMuchText,
21
+ content_checks.theme,
22
+ content_checks.body,
23
+ text_checks.cta,
24
+ text_checks.tnc,
25
+ visual_checks.gnc
26
+ ]
27
+
28
+ # Collect all results
29
+ all_results = {}
30
+ for component in components:
31
+ try:
32
+ results = component(image_path)
33
+ all_results.update(results)
34
+ except Exception as e:
35
+ logger.error(f"Error in component {component.__name__}: {e}")
36
+ pass
37
+
38
+ # Calculate final classification
39
+ final_classification = 0
40
+ for result in all_results.values():
41
+ if isinstance(result, int):
42
+ if result == 1:
43
+ final_classification = 1
44
+ break
45
+ elif isinstance(result, str):
46
+ if result.startswith('1'):
47
+ final_classification = 1
48
+ break
49
+
50
+ # Determine Pass or Fail
51
+ classification_result = "Fail" if final_classification == 1 else "Pass"
52
+
53
+ # Prepare the table data
54
+ table_data = []
55
+ labels = [
56
+ "Bad Image Quality", "No Ribbon", "Empty/Illegible/Black Tagline", "Multiple Taglines",
57
+ "Incomplete Tagline", "Hyperlink", "Price Tag", "Excessive Emojis", "Too Much Text",
58
+ "Inappropriate Content", "Religious Content", "High Risk Content",
59
+ "Illegal Content", "Competitor References", "Bad CTA", "Terms & Conditions",
60
+ "Visual Gesture or Icon"
61
+ ]
62
+
63
+ # Collect labels responsible for failure
64
+ failure_labels = []
65
+ for label in labels:
66
+ result = all_results.get(label, 0)
67
+
68
+ is_fail = False
69
+ if isinstance(result, int) and result == 1:
70
+ is_fail = True
71
+ elif isinstance(result, str) and result.startswith('1'):
72
+ is_fail = True
73
+
74
+ if is_fail:
75
+ failure_labels.append(label)
76
+
77
+ table_data.append([label, result])
78
+
79
+ # Return the final classification, result table data, and failure labels (if any)
80
+ return classification_result, table_data, failure_labels
81
+
82
+ # ==========================================
83
+ # DUMMY CLASSIFICATION FOR TESTING
84
+ # ==========================================
85
+ def classify_dummy(image_path):
86
+ """
87
+ A dummy classification function that returns random results.
88
+ Useful for testing the frontend without running expensive models.
89
+ """
90
+ # Simulate processing time
91
+ time.sleep(1)
92
+
93
+ all_results = {
94
+ "Bad Image Quality": 1,
95
+ "No Ribbon": random.choice([0, 1]),
96
+ "Empty/Illegible/Black Tagline": 1,
97
+ "Multiple Taglines": 1,
98
+ "Incomplete Tagline": 1,
99
+ "Hyperlink": 1,
100
+ "Price Tag": 1,
101
+ "Excessive Emojis": 1,
102
+ "Too Much Text": 1,
103
+ "Inappropriate Content": 1,
104
+ "Religious Content": 1,
105
+ "High Risk Content": 1,
106
+ "Illegal Content": 1,
107
+ "Competitor References": 0,
108
+ "Bad CTA": 0,
109
+ "Terms & Conditions": 0,
110
+ "Visual Gesture or Icon": 1
111
+ }
112
+
113
+ # Determine Pass/Fail based on results
114
+ final_classification = 0
115
+ for result in all_results.values():
116
+ if isinstance(result, int) and result == 1:
117
+ final_classification = 1
118
+ break
119
+ elif isinstance(result, str) and result.startswith('1'):
120
+ final_classification = 1
121
+ break
122
+
123
+ classification_result = "Fail" if final_classification == 1 else "Pass"
124
+
125
+ # Collect failure labels and prepare table data
126
+ labels = list(all_results.keys())
127
+ failure_labels = []
128
+ table_data = []
129
+
130
+ for label in labels:
131
+ result = all_results[label]
132
+ is_fail = False
133
+ if isinstance(result, int) and result == 1:
134
+ is_fail = True
135
+ elif isinstance(result, str) and result.startswith('1'):
136
+ is_fail = True
137
+
138
+ if is_fail:
139
+ failure_labels.append(label)
140
+
141
+ table_data.append([label, result])
142
+
143
+ return classification_result, table_data, failure_labels
144
+
145
+ # ==========================================
146
+ # TOGGLE CLASSIFIER HERE
147
+ # ==========================================
148
+ # Uncomment the one you want to use
149
+
150
+ # classify = classify_dummy
151
+ classify = classify_real
backend/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)
deployment_guide.md ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Deployment Guide: Hugging Face Spaces
2
+
3
+ This guide will help you deploy your **Prism** application to Hugging Face Spaces using Docker.
4
+
5
+ ## Prerequisites
6
+
7
+ 1. A [Hugging Face account](https://huggingface.co/join).
8
+ 2. Git installed on your computer.
9
+
10
+ ## Step 1: Create a New Space
11
+
12
+ 1. Go to [huggingface.co/new-space](https://huggingface.co/new-space).
13
+ 2. **Space Name**: Enter a name (e.g., `prism-classifier`).
14
+ 3. **License**: Choose a license (e.g., MIT) or leave blank.
15
+ 4. **SDK**: Select **Docker**.
16
+ 5. **Space Hardware**: Select **CPU Basic (Free)** (or upgrade if you need more power for AI models).
17
+ 6. **Visibility**: Public or Private.
18
+ 7. Click **Create Space**.
19
+
20
+ ## Step 2: Setup Git for Deployment
21
+
22
+ You need to link your local project to the Hugging Face Space.
23
+
24
+ 1. Open your terminal in the project root (`d:\My Stuff\Coding Projects\Prism`).
25
+ 2. Initialize Git (if not already done):
26
+ ```bash
27
+ git init
28
+ ```
29
+ 3. Add the Hugging Face remote (replace `YOUR_USERNAME` and `SPACE_NAME`):
30
+ ```bash
31
+ git remote add space https://huggingface.co/spaces/YOUR_USERNAME/SPACE_NAME
32
+ ```
33
+
34
+ ## Step 3: Prepare Files (Already Done!)
35
+
36
+ I have already configured the necessary files for you:
37
+ * **`Dockerfile`**: Builds the React frontend and sets up the Python backend.
38
+ * **`app.py`**: Configured to run on port 7860 (required by HF Spaces).
39
+ * **`requirements.txt`**: Lists all Python dependencies.
40
+
41
+ ## Step 4: Deploy
42
+
43
+ To deploy, simply commit your changes and push to the `space` remote.
44
+
45
+ 1. **Add files**:
46
+ ```bash
47
+ git add .
48
+ ```
49
+ 2. **Commit**:
50
+ ```bash
51
+ git commit -m "Initial deploy to Hugging Face"
52
+ ```
53
+ 3. **Push**:
54
+ ```bash
55
+ git push space master:main
56
+ ```
57
+ *(Note: HF Spaces usually use `main` branch. If your local branch is `master`, use `master:main`. If local is `main`, just `git push space main`)*.
58
+
59
+ > **IMPORTANT: Authentication**
60
+ > When asked for your **Username**, enter your Hugging Face username.
61
+ > When asked for your **Password**, you MUST enter an **Access Token**, not your account password.
62
+ >
63
+ > **How to get a Token:**
64
+ > 1. Go to [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens).
65
+ > 2. Click **Create new token**.
66
+ > 3. Type: **Write**. Name: "Deploy".
67
+ > 4. Copy the token (starts with `hf_...`).
68
+ > 5. Paste this token when the terminal asks for your password.
69
+ >
70
+ > **Troubleshooting: "Updates were rejected"**
71
+ > If you see an error saying "Updates were rejected", it means your Space already has a `README.md` that you don't have locally.
72
+ > For the **first push only**, you can force overwrite it:
73
+ > ```bash
74
+ > git push --force space master:main
75
+ > ```
76
+ >
77
+ > **Troubleshooting: "File larger than 10MB"**
78
+ > If you see an error about `static/background.mp3` being too large, you need **Git LFS**:
79
+ > ```bash
80
+ > git lfs install
81
+ > git lfs track "static/background.mp3"
82
+ > git add .gitattributes
83
+ > git commit --amend --no-edit
84
+ > git push --force space master:main
85
+ > ```
86
+
87
+ ## Step 5: Future Updates
88
+
89
+ To reflect future changes on the live site:
90
+
91
+ 1. Make your changes in the code.
92
+ 2. Run:
93
+ ```bash
94
+ git add .
95
+ git commit -m "Update description"
96
+ git push space master:main
97
+ ```
98
+
99
+ The Space will automatically rebuild and update!
100
+
101
+ ## Troubleshooting
102
+
103
+ * **Build Failures**: Check the "Logs" tab in your Hugging Face Space to see why the build failed.
104
+ * **Large Files**: If you have large model files (>10MB), you might need to use `git lfs`. However, your models seem to be downloaded at runtime, so this shouldn't be an issue.
frontend/App.tsx ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { BrowserRouter as Router, Routes, Route, Navigate, Link } from 'react-router-dom';
3
+ import Hero from './components/Hero';
4
+ import SingleAnalysis from './components/SingleAnalysis';
5
+ import BatchAnalysis from './components/BatchAnalysis';
6
+
7
+ const App: React.FC = () => {
8
+ const [isPlaying, setIsPlaying] = React.useState(false);
9
+ const audioRef = React.useRef<HTMLAudioElement>(null);
10
+
11
+ React.useEffect(() => {
12
+ const audio = audioRef.current;
13
+ if (!audio) return;
14
+
15
+ audio.volume = 0.5;
16
+
17
+ // 1. Try to autoplay immediately
18
+ const playPromise = audio.play();
19
+ if (playPromise !== undefined) {
20
+ playPromise
21
+ .then(() => setIsPlaying(true))
22
+ .catch((error) => {
23
+ console.log("Autoplay blocked. Waiting for user interaction.", error);
24
+ setIsPlaying(false);
25
+ });
26
+ }
27
+
28
+ // 2. Add a global click listener as a fallback
29
+ // As soon as the user clicks ANYWHERE, we start the audio
30
+ const handleUserInteraction = () => {
31
+ if (audio.paused) {
32
+ audio.play()
33
+ .then(() => {
34
+ setIsPlaying(true);
35
+ // Remove listener once successful
36
+ document.removeEventListener('click', handleUserInteraction);
37
+ })
38
+ .catch(e => console.error("Play failed even after interaction:", e));
39
+ }
40
+ };
41
+
42
+ document.addEventListener('click', handleUserInteraction);
43
+
44
+ return () => {
45
+ document.removeEventListener('click', handleUserInteraction);
46
+ };
47
+ }, []);
48
+
49
+ const toggleAudio = () => {
50
+ if (audioRef.current) {
51
+ if (isPlaying) {
52
+ audioRef.current.pause();
53
+ } else {
54
+ audioRef.current.play();
55
+ }
56
+ setIsPlaying(!isPlaying);
57
+ }
58
+ };
59
+
60
+ return (
61
+ <Router>
62
+ <div className="text-white selection:bg-cyan-500/30 selection:text-cyan-200">
63
+ {/* Global Nav / Logo - Fixed and High Z-Index */}
64
+ <div className="fixed top-0 left-0 w-full p-6 z-50 pointer-events-none flex justify-between items-start">
65
+ <Link to="/" className="inline-flex items-center gap-3 px-4 py-2 rounded-full bg-slate-900/80 backdrop-blur-md border border-white/10 shadow-lg pointer-events-auto hover:border-cyan-500/50 transition-all cursor-pointer group">
66
+ {/* Logo Icon */}
67
+ <div className="w-8 h-8 rounded-full flex items-center justify-center overflow-hidden bg-white/5">
68
+ <img src="/static/logo.png" alt="Logo" className="w-full h-full object-cover" />
69
+ </div>
70
+ <span className="font-bold tracking-widest text-lg text-white group-hover:text-cyan-400 transition-colors">Samsung Prism Prototype</span>
71
+ </Link>
72
+
73
+ {/* Audio Toggle */}
74
+ <div className="relative">
75
+ {!isPlaying && (
76
+ <div className="absolute right-14 top-1/2 -translate-y-1/2 z-50 flex items-center">
77
+ {/* Gradient Border Container */}
78
+ <div className="relative p-[2px] rounded-full bg-gradient-to-r from-cyan-400 via-purple-500 to-pink-500 animate-pulse shadow-[0_0_15px_rgba(34,211,238,0.5)]">
79
+ {/* Inner Glass Content */}
80
+ <div className="bg-slate-950/90 backdrop-blur-sm rounded-full px-5 py-2.5 flex items-center gap-2">
81
+ <span className="text-xs font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-200 to-white whitespace-nowrap">
82
+ ✨ Don't miss the magic! 🎧
83
+ </span>
84
+ </div>
85
+ {/* Arrow pointing to button */}
86
+ <div className="absolute top-1/2 -right-1.5 -translate-y-1/2 w-3 h-3 bg-gradient-to-r from-purple-500 to-pink-500 rotate-45 transform origin-center -z-10" />
87
+ </div>
88
+ </div>
89
+ )}
90
+ <button
91
+ onClick={toggleAudio}
92
+ className={`pointer-events-auto w-10 h-10 rounded-full bg-slate-900/80 backdrop-blur-md border border-white/10 flex items-center justify-center hover:bg-white/10 transition-all group ${!isPlaying ? 'animate-pulse ring-2 ring-cyan-500/50' : ''}`}
93
+ title={isPlaying ? "Mute Background Music" : "Play Background Music"}
94
+ >
95
+ {isPlaying ? (
96
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 text-cyan-400 group-hover:scale-110 transition-transform">
97
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.426-1.643 1.087-2.146.24-.184.459-.387.653-.611H6.75z" />
98
+ </svg>
99
+ ) : (
100
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-5 h-5 text-slate-400 group-hover:text-white transition-colors">
101
+ <path strokeLinecap="round" strokeLinejoin="round" d="M17.25 9.75L19.5 12m0 0l2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.426-1.643 1.087-2.146.24-.184.459-.387.653-.611H6.75z" />
102
+ </svg>
103
+ )}
104
+ </button>
105
+ </div>
106
+ </div>
107
+
108
+ <audio
109
+ ref={audioRef}
110
+ src="/static/background.mp3"
111
+ loop
112
+ onError={(e) => console.error("Audio failed to load:", e)}
113
+ />
114
+
115
+ <main className="animate-fade-in">
116
+ <Routes>
117
+ <Route path="/" element={<Hero />} />
118
+ <Route path="/single" element={<SingleAnalysis />} />
119
+ <Route path="/batch" element={<BatchAnalysis />} />
120
+ <Route path="*" element={<Navigate to="/" replace />} />
121
+ </Routes>
122
+ </main>
123
+ </div>
124
+ </Router>
125
+ );
126
+ };
127
+
128
+ export default App;
frontend/components/BackgroundAnimation.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useRef } from 'react';
2
+
3
+ const BackgroundAnimation: React.FC = () => {
4
+ const canvasRef = useRef<HTMLCanvasElement>(null);
5
+
6
+ useEffect(() => {
7
+ const canvas = canvasRef.current;
8
+ if (!canvas) return;
9
+
10
+ const ctx = canvas.getContext('2d');
11
+ if (!ctx) return;
12
+
13
+ let width = window.innerWidth;
14
+ let height = window.innerHeight;
15
+ canvas.width = width;
16
+ canvas.height = height;
17
+
18
+ // Star parameters
19
+ const numStars = 400;
20
+ const speed = 2; // Speed of travel
21
+ const stars: { x: number; y: number; z: number; o: number }[] = [];
22
+
23
+ // Initialize stars
24
+ for (let i = 0; i < numStars; i++) {
25
+ stars.push({
26
+ x: Math.random() * width - width / 2,
27
+ y: Math.random() * height - height / 2,
28
+ z: Math.random() * width, // Depth
29
+ o: Math.random(), // Original z for resetting
30
+ });
31
+ }
32
+
33
+ const animate = () => {
34
+ // Clear screen with a slight fade trail for motion blur effect (optional, using clearRect for crispness now)
35
+ ctx.fillStyle = '#020617'; // Match slate-950
36
+ ctx.fillRect(0, 0, width, height);
37
+
38
+ const cx = width / 2;
39
+ const cy = height / 2;
40
+
41
+ stars.forEach((star) => {
42
+ // Move star closer
43
+ star.z -= speed;
44
+
45
+ // Reset if it passes the screen
46
+ if (star.z <= 0) {
47
+ star.z = width;
48
+ star.x = Math.random() * width - width / 2;
49
+ star.y = Math.random() * height - height / 2;
50
+ }
51
+
52
+ // Project 3D to 2D
53
+ // The factor 'width / star.z' makes things bigger as they get closer (z decreases)
54
+ const x = cx + (star.x / star.z) * width;
55
+ const y = cy + (star.y / star.z) * width;
56
+
57
+ // Calculate size based on proximity
58
+ const size = (1 - star.z / width) * 3;
59
+
60
+ // Calculate opacity based on proximity (fade in as they appear)
61
+ const opacity = (1 - star.z / width);
62
+
63
+ // Draw star
64
+ if (x >= 0 && x <= width && y >= 0 && y <= height) {
65
+ ctx.beginPath();
66
+ ctx.fillStyle = `rgba(255, 255, 255, ${opacity})`;
67
+ ctx.arc(x, y, size, 0, Math.PI * 2);
68
+ ctx.fill();
69
+ }
70
+ });
71
+
72
+ requestAnimationFrame(animate);
73
+ };
74
+
75
+ const animationId = requestAnimationFrame(animate);
76
+
77
+ const handleResize = () => {
78
+ width = window.innerWidth;
79
+ height = window.innerHeight;
80
+ canvas.width = width;
81
+ canvas.height = height;
82
+ };
83
+
84
+ window.addEventListener('resize', handleResize);
85
+
86
+ return () => {
87
+ cancelAnimationFrame(animationId);
88
+ window.removeEventListener('resize', handleResize);
89
+ };
90
+ }, []);
91
+
92
+ return (
93
+ <div className="fixed inset-0 overflow-hidden pointer-events-none z-0 bg-slate-950">
94
+ <canvas ref={canvasRef} className="absolute inset-0" />
95
+
96
+ {/* Subtle Nebula Overlay for atmosphere */}
97
+ <div className="absolute top-0 left-1/4 w-[600px] h-[600px] bg-blue-600/10 rounded-full mix-blend-screen filter blur-[120px] opacity-30 animate-blob" />
98
+ <div className="absolute bottom-0 right-1/4 w-[600px] h-[600px] bg-purple-600/10 rounded-full mix-blend-screen filter blur-[120px] opacity-30 animate-blob animation-delay-2000" />
99
+ </div>
100
+ );
101
+ };
102
+
103
+ export default BackgroundAnimation;
frontend/components/BatchAnalysis.tsx ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { UploadIcon, StackIcon, DownloadIcon, ArrowLeftIcon, CheckCircleIcon, XCircleIcon } from './Icons';
4
+ import { BatchItem } from '../types';
5
+ import { uploadMultiple, classifyMultipleStream, clearUploads, getSamples, useSample } from '../services/apiService';
6
+
7
+ const BatchAnalysis: React.FC = () => {
8
+ const navigate = useNavigate();
9
+ const [items, setItems] = useState<BatchItem[]>([]);
10
+ const [processing, setProcessing] = useState(false);
11
+ const [showSamples, setShowSamples] = useState(false);
12
+ const [samples, setSamples] = useState<{ id: number, path: string, name: string }[]>([]);
13
+ const fileInputRef = useRef<HTMLInputElement>(null);
14
+
15
+ useEffect(() => {
16
+ const fetchSamples = async () => {
17
+ try {
18
+ const data = await getSamples();
19
+ if (Array.isArray(data)) {
20
+ setSamples(data);
21
+ }
22
+ } catch (err) {
23
+ console.error("Failed to fetch samples", err);
24
+ }
25
+ };
26
+ fetchSamples();
27
+ }, []);
28
+
29
+ const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
30
+ if (e.target.files && e.target.files.length > 0) {
31
+ const newFiles = Array.from(e.target.files) as File[];
32
+
33
+ // Create preview items
34
+ const newItems: BatchItem[] = newFiles.map(file => ({
35
+ id: Math.random().toString(36).substr(2, 9),
36
+ file: file,
37
+ previewUrl: URL.createObjectURL(file),
38
+ status: 'pending'
39
+ }));
40
+
41
+ setItems(prev => [...prev, ...newItems]);
42
+
43
+ // Upload files immediately
44
+ try {
45
+ await uploadMultiple(newFiles);
46
+ } catch (err) {
47
+ console.error("Upload failed", err);
48
+ // Mark these items as error
49
+ setItems(prev => prev.map(item =>
50
+ newItems.find(ni => ni.id === item.id) ? { ...item, status: 'error' } : item
51
+ ));
52
+ }
53
+ }
54
+ };
55
+
56
+ const addSampleToQueue = async (filename: string, url: string) => {
57
+ try {
58
+ // Call backend to copy sample
59
+ await useSample(filename, 'multiple');
60
+
61
+ // Create a dummy file object for UI state consistency
62
+ // The backend already has the file, so we don't need actual content here
63
+ const file = new File([""], filename, { type: "image/png" });
64
+
65
+ const newItem: BatchItem = {
66
+ id: Math.random().toString(36).substr(2, 9),
67
+ file,
68
+ previewUrl: url,
69
+ status: 'pending'
70
+ };
71
+
72
+ setItems(prev => [...prev, newItem]);
73
+
74
+ } catch (err) {
75
+ console.error("Failed to load sample", err);
76
+ }
77
+ };
78
+
79
+ const normalizeFilename = (name: string) => {
80
+ // Basic emulation of werkzeug.secure_filename behavior
81
+ // 1. ASCII only (remove non-ascii) - simplified here to just keep standard chars
82
+ // 2. Replace whitespace with underscore
83
+ // 3. Remove invalid chars
84
+ let normalized = name.replace(/\s+/g, '_');
85
+ normalized = normalized.replace(/[^a-zA-Z0-9._-]/g, '');
86
+ return normalized;
87
+ };
88
+
89
+ const runBatchProcessing = async () => {
90
+ setProcessing(true);
91
+ setItems(prev => prev.map(item => ({ ...item, status: 'processing', error: undefined })));
92
+
93
+ try {
94
+ // Use the generator helper which handles buffering and parsing correctly
95
+ for await (const result of classifyMultipleStream()) {
96
+ console.log("Received result:", result);
97
+
98
+ if (result.error) {
99
+ console.error("Error for file:", result.filename, result.error);
100
+ setItems(prev => prev.map(item => {
101
+ // Check exact match or normalized match
102
+ if (item.file.name === result.filename || normalizeFilename(item.file.name) === result.filename) {
103
+ return { ...item, status: 'error', error: result.error };
104
+ }
105
+ return item;
106
+ }));
107
+ continue;
108
+ }
109
+
110
+ setItems(prev => prev.map(item => {
111
+ // Check exact match or normalized match
112
+ if (item.file.name === result.filename || normalizeFilename(item.file.name) === result.filename) {
113
+ return {
114
+ ...item,
115
+ status: 'completed',
116
+ result: result.status === 'pass' ? 'pass' : 'fail',
117
+ labels: result.labels
118
+ };
119
+ }
120
+ return item;
121
+ }));
122
+ }
123
+
124
+ } catch (err) {
125
+ console.error("Batch processing error:", err);
126
+ setItems(prev => prev.map(item =>
127
+ item.status === 'processing' ? { ...item, status: 'error', error: 'Network or server error' } : item
128
+ ));
129
+ } finally {
130
+ setProcessing(false);
131
+ // Safety check: Mark any remaining processing items as error
132
+ setItems(prev => prev.map(item =>
133
+ item.status === 'processing' ? {
134
+ ...item,
135
+ status: 'error',
136
+ error: 'No result from server (Filename mismatch or timeout)'
137
+ } : item
138
+ ));
139
+ }
140
+ };
141
+
142
+ const getProgress = () => {
143
+ if (items.length === 0) return 0;
144
+ const completed = items.filter(i => i.status === 'completed' || i.status === 'error').length;
145
+ return (completed / items.length) * 100;
146
+ };
147
+
148
+ const downloadReport = () => {
149
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
150
+ const htmlContent = `
151
+ <!DOCTYPE html>
152
+ <html>
153
+ <head>
154
+ <title>Prism Batch Report - ${timestamp}</title>
155
+ <style>
156
+ body { font-family: sans-serif; background: #f8fafc; padding: 40px; }
157
+ h1 { color: #0f172a; }
158
+ table { width: 100%; border-collapse: collapse; background: white; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1); border-radius: 8px; overflow: hidden; }
159
+ th { background: #1e293b; color: white; text-align: left; padding: 12px 20px; }
160
+ td { border-bottom: 1px solid #e2e8f0; padding: 12px 20px; color: #334155; }
161
+ .pass { color: #059669; font-weight: bold; }
162
+ .fail { color: #e11d48; font-weight: bold; }
163
+ .labels { font-family: monospace; background: #f1f5f9; padding: 2px 6px; rounded: 4px; color: #475569; }
164
+ </style>
165
+ </head>
166
+ <body>
167
+ <h1>Batch Classification Report</h1>
168
+ <p>Generated on: ${new Date().toLocaleString()}</p>
169
+ <table>
170
+ <thead>
171
+ <tr>
172
+ <th>Filename</th>
173
+ <th>Status</th>
174
+ <th>Result</th>
175
+ <th>Failure Reason</th>
176
+ </tr>
177
+ </thead>
178
+ <tbody>
179
+ ${items.map(item => `
180
+ <tr>
181
+ <td>${item.file.name}</td>
182
+ <td>${item.status}</td>
183
+ <td class="${item.result}">${item.result ? item.result.toUpperCase() : '-'}</td>
184
+ <td>${item.labels && item.labels.length > 0 ? `<span class="labels">${item.labels.join(', ')}</span>` : '-'}</td>
185
+ </tr>
186
+ `).join('')}
187
+ </tbody>
188
+ </table>
189
+ </body>
190
+ </html>
191
+ `;
192
+
193
+ const blob = new Blob([htmlContent], { type: 'text/html' });
194
+ const url = URL.createObjectURL(blob);
195
+ const a = document.createElement('a');
196
+ a.href = url;
197
+ a.download = `prism-batch-report-${timestamp}.html`;
198
+ document.body.appendChild(a);
199
+ a.click();
200
+ document.body.removeChild(a);
201
+ URL.revokeObjectURL(url);
202
+ };
203
+
204
+ const clearAll = async () => {
205
+ setItems([]);
206
+ await clearUploads();
207
+ };
208
+
209
+ const isComplete = items.length > 0 && items.every(i => i.status === 'completed' || i.status === 'error');
210
+
211
+ return (
212
+ <div className="min-h-screen flex flex-col p-4 md:p-8 max-w-7xl mx-auto">
213
+ <header className="flex items-center justify-between mb-8">
214
+ <h2 className="text-2xl font-light tracking-wide">Batch Image <span className="font-bold text-cyan-400">Analysis</span></h2>
215
+ </header>
216
+
217
+ {/* Controls */}
218
+ <div className="glass-panel rounded-2xl p-6 mb-8">
219
+ <div className="flex flex-col md:flex-row items-center justify-between gap-6">
220
+ <div className="flex items-center gap-4 w-full md:w-auto">
221
+ <button
222
+ onClick={() => fileInputRef.current?.click()}
223
+ className="flex items-center gap-2 bg-cyan-500 hover:bg-cyan-400 text-black font-bold py-3 px-6 rounded-lg transition-all hover:shadow-[0_0_20px_rgba(34,211,238,0.4)]"
224
+ >
225
+ <UploadIcon /> Upload Files
226
+ </button>
227
+ <input
228
+ type="file"
229
+ ref={fileInputRef}
230
+ className="hidden"
231
+ multiple
232
+ accept="image/*"
233
+ onChange={handleFileChange}
234
+ />
235
+
236
+ {items.length > 0 && (
237
+ <button
238
+ onClick={clearAll}
239
+ className="text-slate-400 hover:text-white transition-colors text-sm"
240
+ >
241
+ Clear Queue
242
+ </button>
243
+ )}
244
+ </div>
245
+
246
+ <div className="flex items-center gap-4 w-full md:w-auto">
247
+ <div className="flex-1 md:w-64 h-2 bg-slate-700 rounded-full overflow-hidden">
248
+ <div
249
+ className="h-full bg-cyan-400 transition-all duration-500 ease-out"
250
+ style={{ width: `${getProgress()}%` }}
251
+ />
252
+ </div>
253
+ <span className="text-sm font-mono text-cyan-400 w-12">{Math.round(getProgress())}%</span>
254
+ </div>
255
+ </div>
256
+
257
+ {/* Sample Gallery Toggle */}
258
+ <button
259
+ onClick={() => setShowSamples(!showSamples)}
260
+ className="mt-6 w-full py-2 border-t border-white/5 text-slate-400 hover:text-cyan-400 text-sm uppercase tracking-widest font-medium transition-colors flex items-center justify-center gap-2"
261
+ >
262
+ <StackIcon />
263
+ {showSamples ? 'Close Test Deck' : 'Load Test Data'}
264
+ </button>
265
+
266
+ <div className={`w-full transition-all duration-500 ease-in-out overflow-hidden ${showSamples ? 'max-h-[400px] opacity-100' : 'max-h-0 opacity-0'}`}>
267
+ <div className="p-6 bg-slate-800/30 rounded-b-2xl border-x border-b border-slate-700/50">
268
+ <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4 overflow-y-auto max-h-[350px] pr-2 custom-scrollbar">
269
+ {samples.map((sample) => {
270
+ const isSelected = items.some(item => item.previewUrl === sample.url);
271
+ return (
272
+ <div
273
+ key={sample.id}
274
+ className={`group relative aspect-square rounded-xl overflow-hidden cursor-pointer border-2 transition-all duration-300 ${isSelected ? 'border-cyan-400 ring-2 ring-cyan-400/50' : 'border-slate-700 hover:border-cyan-500'
275
+ }`}
276
+ onClick={() => addSampleToQueue(sample.filename, sample.url)}
277
+ >
278
+ <img
279
+ src={sample.url}
280
+ alt={`Sample ${sample.id}`}
281
+ className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
282
+ />
283
+ <div className={`absolute inset-0 transition-colors duration-300 ${isSelected ? 'bg-cyan-500/20' : 'bg-black/0 group-hover:bg-black/20'
284
+ }`}>
285
+ {isSelected && (
286
+ <div className="absolute top-2 right-2 bg-cyan-500 rounded-full p-1">
287
+ <CheckCircleIcon className="w-4 h-4 text-white" />
288
+ </div>
289
+ )}
290
+ </div>
291
+ </div>
292
+ );
293
+ })}
294
+ </div>
295
+ </div>
296
+ </div>
297
+ </div>
298
+
299
+ {/* Status Bar */}
300
+ {items.length > 0 && (
301
+ <div className="flex items-center justify-between mb-6 animate-fade-in">
302
+ <div>
303
+ <p className="text-white font-medium">{items.length} items in queue</p>
304
+ {processing && (
305
+ <p className="text-[10px] text-center text-purple-300/80 animate-pulse">
306
+ Running on CPU: Classification takes time, please be patient 🐨✨
307
+ </p>
308
+ )}
309
+ </div>
310
+ <div className="flex gap-4">
311
+ <button
312
+ onClick={runBatchProcessing}
313
+ disabled={processing || isComplete}
314
+ className="bg-white text-black font-bold py-2 px-6 rounded-lg hover:bg-slate-200 disabled:opacity-50 disabled:cursor-not-allowed transition-colors shadow-[0_0_20px_rgba(255,255,255,0.2)]"
315
+ >
316
+ {processing ? 'Processing...' : isComplete ? 'Analysis Complete' : 'Start Analysis'}
317
+ </button>
318
+ <button
319
+ onClick={downloadReport}
320
+ disabled={!isComplete}
321
+ className="flex items-center gap-2 bg-slate-800 text-white py-2 px-6 rounded-lg border border-slate-700 hover:bg-slate-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
322
+ >
323
+ <DownloadIcon /> Report
324
+ </button>
325
+ </div>
326
+ </div>
327
+ )}
328
+
329
+ {/* Grid */}
330
+ <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4 pb-20">
331
+ {items.map((item) => (
332
+ <div
333
+ key={item.id}
334
+ className={`relative aspect-[9/16] rounded-xl overflow-hidden group border animate-fade-in ${item.status === 'completed'
335
+ ? (item.result === 'pass' ? 'border-emerald-500/50' : 'border-rose-500/50')
336
+ : 'border-white/5'
337
+ }`}
338
+ >
339
+ <img src={item.previewUrl} className="w-full h-full object-cover" alt="Batch Item" />
340
+
341
+ {/* Overlay Status */}
342
+ <div className="absolute inset-0 bg-gradient-to-t from-black/90 to-transparent opacity-80 flex flex-col justify-end p-3">
343
+ {item.status === 'processing' && (
344
+ <span className="text-cyan-400 text-xs font-bold animate-pulse">ANALYZING...</span>
345
+ )}
346
+ {item.status === 'pending' && (
347
+ <span className="text-slate-400 text-xs">PENDING</span>
348
+ )}
349
+ {item.status === 'error' && (
350
+ <div className="flex flex-col">
351
+ <span className="text-rose-400 text-xs font-bold">ERROR</span>
352
+ {item.error && (
353
+ <span className="text-[10px] text-rose-200 leading-tight mt-1 break-words">
354
+ {item.error.length > 50 ? item.error.substring(0, 50) + '...' : item.error}
355
+ </span>
356
+ )}
357
+ </div>
358
+ )}
359
+ {item.status === 'completed' && (
360
+ <div className="flex flex-col gap-1">
361
+ <div className="flex items-center gap-1">
362
+ {item.result === 'pass'
363
+ ? <CheckCircleIcon className="text-emerald-400 w-5 h-5" />
364
+ : <XCircleIcon className="text-rose-400 w-5 h-5" />
365
+ }
366
+ <span className={`text-sm font-bold uppercase ${item.result === 'pass' ? 'text-emerald-400' : 'text-rose-400'}`}>
367
+ {item.result}
368
+ </span>
369
+ </div>
370
+ {item.labels && item.labels.length > 0 && (
371
+ <div className="flex flex-wrap gap-1 mt-1">
372
+ {item.labels.map((label, idx) => (
373
+ <span key={idx} className="text-[10px] bg-rose-500/20 text-rose-200 px-1.5 py-0.5 rounded border border-rose-500/30">
374
+ {label}
375
+ </span>
376
+ ))}
377
+ </div>
378
+ )}
379
+ </div>
380
+ )}
381
+ </div>
382
+ </div>
383
+ ))}
384
+ </div>
385
+ </div>
386
+ );
387
+ };
388
+
389
+ export default BatchAnalysis;
frontend/components/Hero.tsx ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { ImageIcon, StackIcon } from './Icons';
4
+ import BackgroundAnimation from './BackgroundAnimation';
5
+
6
+ const Hero: React.FC = () => {
7
+ const navigate = useNavigate();
8
+
9
+ return (
10
+ <div className="min-h-screen flex flex-col items-center relative overflow-hidden px-4 pt-28 pb-12">
11
+
12
+ <BackgroundAnimation />
13
+
14
+ <div className="z-10 max-w-4xl w-full text-center space-y-12 flex-grow flex flex-col justify-center">
15
+
16
+ <div className="space-y-6">
17
+ <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-white/5 border border-white/10 text-xs tracking-widest uppercase text-cyan-400 mb-4">
18
+ <span className="w-2 h-2 rounded-full bg-cyan-400 animate-ping" />
19
+ v2.0 Prototype
20
+ </div>
21
+
22
+ <h1 className="text-8xl font-black tracking-tighter text-transparent bg-clip-text bg-gradient-to-r from-white via-cyan-200 to-blue-400 mb-2 drop-shadow-2xl">
23
+ PRISM
24
+ </h1>
25
+ <h2 className="text-3xl font-light text-slate-300 tracking-wide">
26
+ Lock Screen Classifier
27
+ </h2>
28
+ <p className="text-lg text-cyan-400/80 uppercase tracking-widest font-medium">
29
+ Automated Compliance for Samsung Glance
30
+ </p>
31
+ </div>
32
+
33
+ <div className="max-w-2xl mx-auto glass-panel p-8 rounded-2xl border-l-4 border-l-cyan-500">
34
+ <p className="text-slate-200 text-lg leading-relaxed font-light">
35
+ "Making classification of lock screens generated by Glance for Samsung
36
+ <span className="font-semibold text-white"> automatic without human intervention</span>,
37
+ saving <span className="font-semibold text-cyan-300">40 hr/week</span> bandwidth."
38
+ </p>
39
+ </div>
40
+
41
+ <div className="grid md:grid-cols-2 gap-6 w-full max-w-3xl mx-auto mt-12">
42
+ <button
43
+ onClick={() => navigate('/single')}
44
+ className="group glass-panel glass-panel-hover rounded-2xl p-6 md:p-8 text-left transition-all duration-300 overflow-hidden h-auto min-h-[240px] flex flex-col"
45
+ >
46
+ <div className="w-14 h-14 rounded-xl bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center mb-6 group-hover:scale-110 transition-transform flex-shrink-0">
47
+ <ImageIcon />
48
+ </div>
49
+ <h3 className="text-2xl font-semibold text-white mb-2 group-hover:text-cyan-300 transition-colors">Single Image Audit</h3>
50
+ <p className="text-slate-400 text-sm break-words leading-relaxed">Deep analysis of individual creative assets with granular compliance checks.</p>
51
+ </button>
52
+
53
+ <button
54
+ onClick={() => navigate('/batch')}
55
+ className="group glass-panel glass-panel-hover rounded-2xl p-6 md:p-8 text-left transition-all duration-300 overflow-hidden h-auto min-h-[240px] flex flex-col"
56
+ >
57
+ <div className="w-14 h-14 rounded-xl bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center mb-6 group-hover:scale-110 transition-transform flex-shrink-0">
58
+ <StackIcon />
59
+ </div>
60
+ <h3 className="text-2xl font-semibold text-white mb-2 group-hover:text-cyan-300 transition-colors">Batch Audit</h3>
61
+ <p className="text-slate-400 text-sm break-words leading-relaxed">High-volume processing for bulk validations. Generate comprehensive reports.</p>
62
+ </button>
63
+ </div>
64
+ </div>
65
+
66
+ {/* Proof of Work Section - Actual Images */}
67
+ <div className="w-full max-w-3xl mx-auto mt-32 z-10 pb-20">
68
+ <div className="text-center mb-12">
69
+ <h3 className="text-xl font-light uppercase tracking-widest text-slate-400">Proof of Work</h3>
70
+ <div className="h-1 w-20 bg-gradient-to-r from-purple-500 to-cyan-500 mx-auto mt-4 rounded-full"></div>
71
+ </div>
72
+
73
+ <div className="flex flex-col gap-12 items-center">
74
+ {/* Certificate 1: Participation (Top) */}
75
+ <div className="relative group w-full transform transition-all hover:scale-[1.01]">
76
+ <div className="absolute -inset-1 bg-gradient-to-r from-[#EAB308] to-yellow-600 rounded-lg blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
77
+ <div className="relative rounded-lg overflow-hidden border-4 border-[#EAB308]/50 shadow-2xl bg-white">
78
+ <img
79
+ src="/static/certificates/participation.png"
80
+ alt="Participation Certificate"
81
+ className="w-full h-full object-cover"
82
+ onError={(e) => {
83
+ (e.target as HTMLImageElement).src = 'https://placehold.co/800x600/1e293b/FFF?text=Upload+Participation+Cert+to+static/certificates/';
84
+ }}
85
+ />
86
+ </div>
87
+ </div>
88
+
89
+ {/* Certificate 2: Excellence (Bottom) */}
90
+ <div className="relative group w-full transform transition-all hover:scale-[1.01]">
91
+ <div className="absolute -inset-1 bg-gradient-to-r from-cyan-500 to-blue-600 rounded-lg blur opacity-25 group-hover:opacity-50 transition duration-1000 group-hover:duration-200"></div>
92
+ <div className="relative rounded-lg overflow-hidden border-4 border-cyan-500/50 shadow-2xl bg-white">
93
+ <img
94
+ src="/static/certificates/excellence.png"
95
+ alt="Certificate of Excellence"
96
+ className="w-full h-auto block"
97
+ onError={(e) => {
98
+ (e.target as HTMLImageElement).src = 'https://placehold.co/800x600/1e293b/FFF?text=Upload+Excellence+Cert+to+static/certificates/';
99
+ }}
100
+ />
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+
106
+ <footer className="mt-12 text-slate-500 text-sm text-center">
107
+ <p className="text-xs mt-2 opacity-50">&copy; made by Devansh Singh</p>
108
+ </footer>
109
+ </div>
110
+ );
111
+ };
112
+
113
+ export default Hero;
frontend/components/Icons.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ export const UploadIcon = () => (
4
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
5
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
6
+ <polyline points="17 8 12 3 7 8"></polyline>
7
+ <line x1="12" y1="3" x2="12" y2="15"></line>
8
+ </svg>
9
+ );
10
+
11
+ export const ImageIcon = () => (
12
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
13
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
14
+ <circle cx="8.5" cy="8.5" r="1.5"></circle>
15
+ <polyline points="21 15 16 10 5 21"></polyline>
16
+ </svg>
17
+ );
18
+
19
+ export const StackIcon = () => (
20
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
21
+ <path d="M3 6h18"></path>
22
+ <path d="M3 12h18"></path>
23
+ <path d="M3 18h18"></path>
24
+ </svg>
25
+ );
26
+
27
+ export const CheckCircleIcon = ({ className }: { className?: string }) => (
28
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
29
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
30
+ <polyline points="22 4 12 14.01 9 11.01"></polyline>
31
+ </svg>
32
+ );
33
+
34
+ export const XCircleIcon = ({ className }: { className?: string }) => (
35
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
36
+ <circle cx="12" cy="12" r="10"></circle>
37
+ <line x1="15" y1="9" x2="9" y2="15"></line>
38
+ <line x1="9" y1="9" x2="15" y2="15"></line>
39
+ </svg>
40
+ );
41
+
42
+ export const ArrowLeftIcon = () => (
43
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
44
+ <line x1="19" y1="12" x2="5" y2="12"></line>
45
+ <polyline points="12 19 5 12 12 5"></polyline>
46
+ </svg>
47
+ );
48
+
49
+ export const ScanIcon = () => (
50
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
51
+ <path d="M3 7V5a2 2 0 0 1 2-2h2"></path>
52
+ <path d="M17 3h2a2 2 0 0 1 2 2v2"></path>
53
+ <path d="M21 17v2a2 2 0 0 1-2 2h-2"></path>
54
+ <path d="M7 21H5a2 2 0 0 1-2-2v-2"></path>
55
+ </svg>
56
+ );
57
+
58
+ export const DownloadIcon = () => (
59
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
60
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
61
+ <polyline points="7 10 12 15 17 10"></polyline>
62
+ <line x1="12" y1="15" x2="12" y2="3"></line>
63
+ </svg>
64
+ );
frontend/components/SingleAnalysis.tsx ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { useNavigate } from 'react-router-dom';
3
+ import { CheckCircleIcon, XCircleIcon, UploadIcon, ScanIcon, ArrowLeftIcon } from './Icons';
4
+ import { SingleAnalysisReport } from '../types';
5
+ import { uploadSingle, classifySingle, getSamples, useSample } from '../services/apiService';
6
+
7
+ interface SingleAnalysisProps {
8
+ onBack: () => void;
9
+ }
10
+
11
+ const SingleAnalysis: React.FC = () => {
12
+ const navigate = useNavigate();
13
+ const [image, setImage] = useState<File | null>(null);
14
+ const [preview, setPreview] = useState<string | null>(null);
15
+ const [loading, setLoading] = useState(false);
16
+ const [report, setReport] = useState<SingleAnalysisReport | null>(null);
17
+ const [samples, setSamples] = useState<{ id: number, url: string, filename: string }[]>([]);
18
+ const fileInputRef = useRef<HTMLInputElement>(null);
19
+
20
+ useEffect(() => {
21
+ const fetchSamples = async () => {
22
+ try {
23
+ const data = await getSamples();
24
+ if (Array.isArray(data)) {
25
+ setSamples(data);
26
+ }
27
+ } catch (err) {
28
+ console.error("Failed to fetch samples", err);
29
+ }
30
+ };
31
+ fetchSamples();
32
+ }, []);
33
+
34
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
35
+ if (e.target.files && e.target.files[0]) {
36
+ const file = e.target.files[0];
37
+ setImage(file);
38
+ setPreview(URL.createObjectURL(file));
39
+ setReport(null);
40
+ }
41
+ };
42
+
43
+ const runClassification = async (filename: string) => {
44
+ setLoading(true);
45
+ try {
46
+ const result = await classifySingle(filename);
47
+ setReport(result);
48
+ } catch (err) {
49
+ console.error(err);
50
+ alert("Analysis failed. Please try again.");
51
+ } finally {
52
+ setLoading(false);
53
+ }
54
+ };
55
+
56
+ const handleUploadAndAnalyze = async () => {
57
+ if (!image) return;
58
+ setLoading(true);
59
+ try {
60
+ const filename = await uploadSingle(image);
61
+ await runClassification(filename);
62
+ } catch (err) {
63
+ console.error(err);
64
+ alert("Upload failed.");
65
+ setLoading(false);
66
+ }
67
+ };
68
+
69
+ const handleSampleSelect = async (filename: string) => {
70
+ setLoading(true);
71
+ try {
72
+ // Call backend to copy sample to uploads folder
73
+ await useSample(filename, 'single');
74
+
75
+ // Set preview
76
+ setPreview(`/static/samples/${filename}`);
77
+ setImage(null); // Clear file input
78
+
79
+ // Run classification on the sample (now in uploads folder)
80
+ await runClassification(filename);
81
+ } catch (err) {
82
+ console.error("Failed to use sample", err);
83
+ alert("Failed to load sample.");
84
+ setLoading(false);
85
+ }
86
+ };
87
+
88
+ const reset = () => {
89
+ setImage(null);
90
+ setPreview(null);
91
+ setReport(null);
92
+ };
93
+
94
+ return (
95
+ <div className="min-h-screen flex flex-col p-4 md:p-8 max-w-7xl mx-auto">
96
+ <header className="flex items-center justify-between mb-8">
97
+ <h2 className="text-2xl font-light tracking-wide">Single Image <span className="font-bold text-cyan-400">Analysis</span></h2>
98
+ </header>
99
+
100
+ <div className="flex flex-col lg:flex-row gap-8 flex-1">
101
+ {/* Left: Upload / Preview */}
102
+ <div className="flex-1 flex flex-col gap-6">
103
+ <div className="glass-panel rounded-3xl p-8 flex flex-col items-center justify-center min-h-[400px] relative overflow-hidden group">
104
+ <input
105
+ type="file"
106
+ ref={fileInputRef}
107
+ onChange={handleFileChange}
108
+ className="hidden"
109
+ accept="image/*"
110
+ />
111
+
112
+ {preview ? (
113
+ <div className="relative w-full h-full flex items-center justify-center">
114
+ <img
115
+ src={preview}
116
+ alt="Preview"
117
+ className="max-h-[500px] w-auto object-contain rounded-lg shadow-2xl"
118
+ />
119
+ <button
120
+ onClick={() => {
121
+ setPreview(null);
122
+ setImage(null);
123
+ setReport(null);
124
+ }}
125
+ className="absolute top-4 right-4 p-2 bg-black/50 hover:bg-black/70 rounded-full text-white transition-colors"
126
+ >
127
+ <XCircleIcon />
128
+ </button>
129
+ </div>
130
+ ) : (
131
+ <div
132
+ onClick={() => fileInputRef.current?.click()}
133
+ className="cursor-pointer flex flex-col items-center text-center p-8 border-2 border-dashed border-slate-700 hover:border-cyan-500 rounded-2xl transition-colors w-full h-full justify-center"
134
+ >
135
+ <div className="w-20 h-20 bg-slate-800 rounded-full flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300">
136
+ <UploadIcon />
137
+ </div>
138
+ <h3 className="text-xl font-medium text-white mb-2">Upload Image</h3>
139
+ <p className="text-slate-400 max-w-xs">Drag & drop or click to browse</p>
140
+ <p className="text-xs text-slate-500 mt-4">Supports PNG, JPG, JPEG</p>
141
+ </div>
142
+ )}
143
+ </div>
144
+
145
+ {/* Sample Gallery */}
146
+ <div className="glass-panel rounded-2xl p-6 flex flex-col max-h-[600px] min-h-0">
147
+ <p className="text-sm text-slate-400 mb-4 uppercase tracking-wider font-semibold flex-shrink-0">Or try a sample</p>
148
+ <div className="grid grid-cols-2 gap-4 overflow-y-auto custom-scrollbar pr-2 flex-1">
149
+ {samples.map((sample) => {
150
+ const isSelected = preview === sample.url;
151
+ return (
152
+ <div
153
+ key={sample.id}
154
+ className={`group relative w-full h-64 rounded-xl overflow-hidden cursor-pointer border-2 transition-all duration-300 ${isSelected ? 'border-cyan-400 ring-2 ring-cyan-400/50' : 'border-slate-700 hover:border-cyan-500'
155
+ }`}
156
+ onClick={() => handleSampleSelect(sample.filename)}
157
+ >
158
+ <img
159
+ src={sample.url}
160
+ alt={`Sample ${sample.id}`}
161
+ className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
162
+ />
163
+ <div className={`absolute inset-0 transition-colors duration-300 ${isSelected ? 'bg-cyan-500/20' : 'bg-black/0 group-hover:bg-black/20'
164
+ }`}>
165
+ {isSelected && (
166
+ <div className="absolute top-2 right-2 bg-cyan-500 rounded-full p-1">
167
+ <CheckCircleIcon className="w-4 h-4 text-white" />
168
+ </div>
169
+ )}
170
+ </div>
171
+ </div>
172
+ );
173
+ })}
174
+ </div>
175
+ </div>
176
+ </div>
177
+
178
+ {/* Right: Report Area */}
179
+ <div className="flex flex-col">
180
+ {!report && (
181
+ <div className="flex-1 flex flex-col items-center justify-center glass-panel rounded-3xl p-12 text-center">
182
+ <div className="w-16 h-16 bg-slate-800 rounded-2xl flex items-center justify-center mb-4 text-slate-500">
183
+ <ScanIcon />
184
+ </div>
185
+ <p className="text-slate-400 text-lg">Upload an image or select a sample to generate a compliance report.</p>
186
+
187
+ {image && !loading && (
188
+ <button
189
+ onClick={handleUploadAndAnalyze}
190
+ className="mt-8 bg-cyan-500 hover:bg-cyan-400 text-black font-bold py-4 px-12 rounded-xl transition-all hover:shadow-[0_0_30px_rgba(34,211,238,0.4)]"
191
+ >
192
+ Run Classification
193
+ </button>
194
+ )}
195
+ </div>
196
+ )}
197
+
198
+ {report && (
199
+ <div className="flex-1 glass-panel rounded-3xl p-8 overflow-y-auto animate-fade-in flex flex-col">
200
+ <div className="flex items-center justify-between mb-6 pb-4 border-b border-white/10">
201
+ <h3 className="text-xl font-semibold text-white">Compliance Report</h3>
202
+ <div className="px-3 py-1 rounded-full bg-cyan-500/10 text-cyan-400 text-sm border border-cyan-500/20">
203
+ AI Verified
204
+ </div>
205
+ </div>
206
+
207
+ {/* DEBUG: Check what we are receiving */}
208
+ {/* <pre className="text-xs text-slate-500 mb-4 overflow-auto max-h-20">{JSON.stringify(report, null, 2)}</pre> */}
209
+
210
+ {/* Render Tailwind Table */}
211
+ <div className="flex-1 overflow-x-auto">
212
+ <table className="w-full text-left border-collapse">
213
+ <thead>
214
+ <tr className="border-b border-white/10 text-slate-400 text-sm uppercase tracking-wider">
215
+ <th className="py-3 px-4 font-medium">Label</th>
216
+ <th className="py-3 px-4 font-medium text-right">Result</th>
217
+ </tr>
218
+ </thead>
219
+ <tbody className="divide-y divide-white/5">
220
+ {report.detailed_results && report.detailed_results.map(([label, result], index) => {
221
+ const isFail = String(result).startsWith('1') || result === 1;
222
+ return (
223
+ <tr key={index} className="hover:bg-white/5 transition-colors">
224
+ <td className="py-3 px-4 text-slate-300 font-medium">{label}</td>
225
+ <td className="py-3 px-4 text-right">
226
+ <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${isFail
227
+ ? 'bg-red-500/10 text-red-400 border border-red-500/20'
228
+ : 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20'
229
+ }`}>
230
+ {isFail ? 'Fail' : 'Pass'}
231
+ </span>
232
+ </td>
233
+ </tr>
234
+ );
235
+ })}
236
+ </tbody>
237
+ </table>
238
+ </div>
239
+
240
+ <div className="mt-8 pt-6 border-t border-white/10">
241
+ <button onClick={reset} className="w-full py-4 rounded-xl bg-white/5 hover:bg-white/10 text-white font-medium transition-colors border border-white/10">
242
+ Analyze Another Image
243
+ </button>
244
+ </div>
245
+ </div>
246
+ )}
247
+ </div>
248
+ </div>
249
+ </div>
250
+ );
251
+ };
252
+
253
+ export default SingleAnalysis;
frontend/index.html ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <link rel="icon" type="image/png" href="/favicon.png" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <title>Prototype v2.0</title>
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
11
+ <style>
12
+ body {
13
+ font-family: 'Inter', sans-serif;
14
+ background-color: #020617;
15
+ /* Slate 950 */
16
+ color: #f8fafc;
17
+ }
18
+
19
+ .glass-panel {
20
+ background: rgba(255, 255, 255, 0.03);
21
+ backdrop-filter: blur(16px);
22
+ -webkit-backdrop-filter: blur(16px);
23
+ border: 1px solid rgba(255, 255, 255, 0.08);
24
+ }
25
+
26
+ .glass-panel-hover:hover {
27
+ background: rgba(255, 255, 255, 0.07);
28
+ border-color: rgba(255, 255, 255, 0.2);
29
+ transform: translateY(-2px);
30
+ box-shadow: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
31
+ }
32
+
33
+ /* Custom Scrollbar */
34
+ ::-webkit-scrollbar {
35
+ width: 8px;
36
+ }
37
+
38
+ ::-webkit-scrollbar-track {
39
+ background: #0f172a;
40
+ }
41
+
42
+ ::-webkit-scrollbar-thumb {
43
+ background: #334155;
44
+ border-radius: 4px;
45
+ }
46
+
47
+ ::-webkit-scrollbar-thumb:hover {
48
+ background: #475569;
49
+ }
50
+
51
+ .animated-gradient-text {
52
+ background: linear-gradient(to right, #c084fc, #22d3ee, #c084fc);
53
+ -webkit-background-clip: text;
54
+ background-clip: text;
55
+ color: transparent;
56
+ background-size: 200% auto;
57
+ animation: gradient 4s linear infinite;
58
+ }
59
+
60
+ @keyframes gradient {
61
+ 0% {
62
+ background-position: 0% 50%;
63
+ }
64
+
65
+ 100% {
66
+ background-position: 200% 50%;
67
+ }
68
+ }
69
+
70
+ @keyframes blob {
71
+ 0% {
72
+ transform: translate(0px, 0px) scale(1);
73
+ }
74
+
75
+ 33% {
76
+ transform: translate(30px, -50px) scale(1.1);
77
+ }
78
+
79
+ 66% {
80
+ transform: translate(-20px, 20px) scale(0.9);
81
+ }
82
+
83
+ 100% {
84
+ transform: translate(0px, 0px) scale(1);
85
+ }
86
+ }
87
+
88
+ @keyframes drift {
89
+ 0% {
90
+ transform: rotate(0deg);
91
+ }
92
+
93
+ 100% {
94
+ transform: rotate(360deg);
95
+ }
96
+ }
97
+
98
+ .animate-blob {
99
+ animation: blob 7s infinite;
100
+ }
101
+
102
+ .animation-delay-2000 {
103
+ animation-delay: 2s;
104
+ }
105
+
106
+ @keyframes twinkle {
107
+
108
+ 0%,
109
+ 100% {
110
+ opacity: 1;
111
+ transform: scale(1);
112
+ }
113
+
114
+ 50% {
115
+ opacity: 0.3;
116
+ transform: scale(0.5);
117
+ }
118
+ }
119
+
120
+ @keyframes warp {
121
+ 0% {
122
+ transform: translateZ(-1000px);
123
+ opacity: 0;
124
+ }
125
+
126
+ 10% {
127
+ opacity: 1;
128
+ }
129
+
130
+ 100% {
131
+ transform: translateZ(1000px);
132
+ opacity: 0;
133
+ }
134
+ }
135
+
136
+ .animate-warp {
137
+ animation: warp 3s linear infinite;
138
+ }
139
+
140
+ /* Backend Generated Table Styling */
141
+ .generated-table-container table {
142
+ width: 100%;
143
+ border-collapse: collapse;
144
+ font-size: 0.95rem;
145
+ }
146
+
147
+ .generated-table-container th {
148
+ text-align: left;
149
+ padding: 1rem;
150
+ color: #22d3ee;
151
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
152
+ font-weight: 600;
153
+ text-transform: uppercase;
154
+ letter-spacing: 0.05em;
155
+ font-size: 0.8rem;
156
+ }
157
+
158
+ .generated-table-container td {
159
+ padding: 1rem;
160
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
161
+ color: #e2e8f0;
162
+ }
163
+
164
+ .generated-table-container tr:last-child td {
165
+ border-bottom: none;
166
+ }
167
+
168
+ .generated-table-container tr:hover td {
169
+ background: rgba(255, 255, 255, 0.02);
170
+ }
171
+ </style>
172
+ <script type="importmap">
173
+ {
174
+ "imports": {
175
+ "@google/genai": "https://aistudiocdn.com/@google/genai@^1.30.0",
176
+ "react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
177
+ "react/": "https://aistudiocdn.com/react@^19.2.0/",
178
+ "react": "https://aistudiocdn.com/react@^19.2.0"
179
+ }
180
+ }
181
+ </script>
182
+ <link rel="stylesheet" href="/index.css">
183
+ </head>
184
+
185
+ <body>
186
+ <div id="root"></div>
187
+ <script type="module" src="/index.tsx"></script>
188
+ </body>
189
+
190
+ </html>
frontend/index.tsx ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+
5
+ const rootElement = document.getElementById('root');
6
+ if (!rootElement) {
7
+ throw new Error("Could not find root element to mount to");
8
+ }
9
+
10
+ const root = ReactDOM.createRoot(rootElement);
11
+ root.render(
12
+ <React.StrictMode>
13
+ <App />
14
+ </React.StrictMode>
15
+ );
frontend/package-lock.json ADDED
@@ -0,0 +1,2441 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "prism---lock-screen-classifier",
3
+ "version": "0.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "prism---lock-screen-classifier",
9
+ "version": "0.0.0",
10
+ "dependencies": {
11
+ "@google/genai": "^1.30.0",
12
+ "react": "^19.2.0",
13
+ "react-dom": "^19.2.0",
14
+ "react-router-dom": "^7.9.6"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^22.14.0",
18
+ "@vitejs/plugin-react": "^5.0.0",
19
+ "typescript": "~5.8.2",
20
+ "vite": "^6.2.0"
21
+ }
22
+ },
23
+ "node_modules/@babel/code-frame": {
24
+ "version": "7.27.1",
25
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
26
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
27
+ "dev": true,
28
+ "dependencies": {
29
+ "@babel/helper-validator-identifier": "^7.27.1",
30
+ "js-tokens": "^4.0.0",
31
+ "picocolors": "^1.1.1"
32
+ },
33
+ "engines": {
34
+ "node": ">=6.9.0"
35
+ }
36
+ },
37
+ "node_modules/@babel/compat-data": {
38
+ "version": "7.28.5",
39
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
40
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
41
+ "dev": true,
42
+ "engines": {
43
+ "node": ">=6.9.0"
44
+ }
45
+ },
46
+ "node_modules/@babel/core": {
47
+ "version": "7.28.5",
48
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
49
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
50
+ "dev": true,
51
+ "dependencies": {
52
+ "@babel/code-frame": "^7.27.1",
53
+ "@babel/generator": "^7.28.5",
54
+ "@babel/helper-compilation-targets": "^7.27.2",
55
+ "@babel/helper-module-transforms": "^7.28.3",
56
+ "@babel/helpers": "^7.28.4",
57
+ "@babel/parser": "^7.28.5",
58
+ "@babel/template": "^7.27.2",
59
+ "@babel/traverse": "^7.28.5",
60
+ "@babel/types": "^7.28.5",
61
+ "@jridgewell/remapping": "^2.3.5",
62
+ "convert-source-map": "^2.0.0",
63
+ "debug": "^4.1.0",
64
+ "gensync": "^1.0.0-beta.2",
65
+ "json5": "^2.2.3",
66
+ "semver": "^6.3.1"
67
+ },
68
+ "engines": {
69
+ "node": ">=6.9.0"
70
+ },
71
+ "funding": {
72
+ "type": "opencollective",
73
+ "url": "https://opencollective.com/babel"
74
+ }
75
+ },
76
+ "node_modules/@babel/generator": {
77
+ "version": "7.28.5",
78
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
79
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
80
+ "dev": true,
81
+ "dependencies": {
82
+ "@babel/parser": "^7.28.5",
83
+ "@babel/types": "^7.28.5",
84
+ "@jridgewell/gen-mapping": "^0.3.12",
85
+ "@jridgewell/trace-mapping": "^0.3.28",
86
+ "jsesc": "^3.0.2"
87
+ },
88
+ "engines": {
89
+ "node": ">=6.9.0"
90
+ }
91
+ },
92
+ "node_modules/@babel/helper-compilation-targets": {
93
+ "version": "7.27.2",
94
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
95
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
96
+ "dev": true,
97
+ "dependencies": {
98
+ "@babel/compat-data": "^7.27.2",
99
+ "@babel/helper-validator-option": "^7.27.1",
100
+ "browserslist": "^4.24.0",
101
+ "lru-cache": "^5.1.1",
102
+ "semver": "^6.3.1"
103
+ },
104
+ "engines": {
105
+ "node": ">=6.9.0"
106
+ }
107
+ },
108
+ "node_modules/@babel/helper-globals": {
109
+ "version": "7.28.0",
110
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
111
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
112
+ "dev": true,
113
+ "engines": {
114
+ "node": ">=6.9.0"
115
+ }
116
+ },
117
+ "node_modules/@babel/helper-module-imports": {
118
+ "version": "7.27.1",
119
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
120
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
121
+ "dev": true,
122
+ "dependencies": {
123
+ "@babel/traverse": "^7.27.1",
124
+ "@babel/types": "^7.27.1"
125
+ },
126
+ "engines": {
127
+ "node": ">=6.9.0"
128
+ }
129
+ },
130
+ "node_modules/@babel/helper-module-transforms": {
131
+ "version": "7.28.3",
132
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
133
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
134
+ "dev": true,
135
+ "dependencies": {
136
+ "@babel/helper-module-imports": "^7.27.1",
137
+ "@babel/helper-validator-identifier": "^7.27.1",
138
+ "@babel/traverse": "^7.28.3"
139
+ },
140
+ "engines": {
141
+ "node": ">=6.9.0"
142
+ },
143
+ "peerDependencies": {
144
+ "@babel/core": "^7.0.0"
145
+ }
146
+ },
147
+ "node_modules/@babel/helper-plugin-utils": {
148
+ "version": "7.27.1",
149
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
150
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
151
+ "dev": true,
152
+ "engines": {
153
+ "node": ">=6.9.0"
154
+ }
155
+ },
156
+ "node_modules/@babel/helper-string-parser": {
157
+ "version": "7.27.1",
158
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
159
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
160
+ "dev": true,
161
+ "engines": {
162
+ "node": ">=6.9.0"
163
+ }
164
+ },
165
+ "node_modules/@babel/helper-validator-identifier": {
166
+ "version": "7.28.5",
167
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
168
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
169
+ "dev": true,
170
+ "engines": {
171
+ "node": ">=6.9.0"
172
+ }
173
+ },
174
+ "node_modules/@babel/helper-validator-option": {
175
+ "version": "7.27.1",
176
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
177
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
178
+ "dev": true,
179
+ "engines": {
180
+ "node": ">=6.9.0"
181
+ }
182
+ },
183
+ "node_modules/@babel/helpers": {
184
+ "version": "7.28.4",
185
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
186
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
187
+ "dev": true,
188
+ "dependencies": {
189
+ "@babel/template": "^7.27.2",
190
+ "@babel/types": "^7.28.4"
191
+ },
192
+ "engines": {
193
+ "node": ">=6.9.0"
194
+ }
195
+ },
196
+ "node_modules/@babel/parser": {
197
+ "version": "7.28.5",
198
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
199
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
200
+ "dev": true,
201
+ "dependencies": {
202
+ "@babel/types": "^7.28.5"
203
+ },
204
+ "bin": {
205
+ "parser": "bin/babel-parser.js"
206
+ },
207
+ "engines": {
208
+ "node": ">=6.0.0"
209
+ }
210
+ },
211
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
212
+ "version": "7.27.1",
213
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
214
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
215
+ "dev": true,
216
+ "dependencies": {
217
+ "@babel/helper-plugin-utils": "^7.27.1"
218
+ },
219
+ "engines": {
220
+ "node": ">=6.9.0"
221
+ },
222
+ "peerDependencies": {
223
+ "@babel/core": "^7.0.0-0"
224
+ }
225
+ },
226
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
227
+ "version": "7.27.1",
228
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
229
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
230
+ "dev": true,
231
+ "dependencies": {
232
+ "@babel/helper-plugin-utils": "^7.27.1"
233
+ },
234
+ "engines": {
235
+ "node": ">=6.9.0"
236
+ },
237
+ "peerDependencies": {
238
+ "@babel/core": "^7.0.0-0"
239
+ }
240
+ },
241
+ "node_modules/@babel/template": {
242
+ "version": "7.27.2",
243
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
244
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
245
+ "dev": true,
246
+ "dependencies": {
247
+ "@babel/code-frame": "^7.27.1",
248
+ "@babel/parser": "^7.27.2",
249
+ "@babel/types": "^7.27.1"
250
+ },
251
+ "engines": {
252
+ "node": ">=6.9.0"
253
+ }
254
+ },
255
+ "node_modules/@babel/traverse": {
256
+ "version": "7.28.5",
257
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
258
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
259
+ "dev": true,
260
+ "dependencies": {
261
+ "@babel/code-frame": "^7.27.1",
262
+ "@babel/generator": "^7.28.5",
263
+ "@babel/helper-globals": "^7.28.0",
264
+ "@babel/parser": "^7.28.5",
265
+ "@babel/template": "^7.27.2",
266
+ "@babel/types": "^7.28.5",
267
+ "debug": "^4.3.1"
268
+ },
269
+ "engines": {
270
+ "node": ">=6.9.0"
271
+ }
272
+ },
273
+ "node_modules/@babel/types": {
274
+ "version": "7.28.5",
275
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
276
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
277
+ "dev": true,
278
+ "dependencies": {
279
+ "@babel/helper-string-parser": "^7.27.1",
280
+ "@babel/helper-validator-identifier": "^7.28.5"
281
+ },
282
+ "engines": {
283
+ "node": ">=6.9.0"
284
+ }
285
+ },
286
+ "node_modules/@esbuild/aix-ppc64": {
287
+ "version": "0.25.12",
288
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
289
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
290
+ "cpu": [
291
+ "ppc64"
292
+ ],
293
+ "dev": true,
294
+ "optional": true,
295
+ "os": [
296
+ "aix"
297
+ ],
298
+ "engines": {
299
+ "node": ">=18"
300
+ }
301
+ },
302
+ "node_modules/@esbuild/android-arm": {
303
+ "version": "0.25.12",
304
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
305
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
306
+ "cpu": [
307
+ "arm"
308
+ ],
309
+ "dev": true,
310
+ "optional": true,
311
+ "os": [
312
+ "android"
313
+ ],
314
+ "engines": {
315
+ "node": ">=18"
316
+ }
317
+ },
318
+ "node_modules/@esbuild/android-arm64": {
319
+ "version": "0.25.12",
320
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
321
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
322
+ "cpu": [
323
+ "arm64"
324
+ ],
325
+ "dev": true,
326
+ "optional": true,
327
+ "os": [
328
+ "android"
329
+ ],
330
+ "engines": {
331
+ "node": ">=18"
332
+ }
333
+ },
334
+ "node_modules/@esbuild/android-x64": {
335
+ "version": "0.25.12",
336
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
337
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
338
+ "cpu": [
339
+ "x64"
340
+ ],
341
+ "dev": true,
342
+ "optional": true,
343
+ "os": [
344
+ "android"
345
+ ],
346
+ "engines": {
347
+ "node": ">=18"
348
+ }
349
+ },
350
+ "node_modules/@esbuild/darwin-arm64": {
351
+ "version": "0.25.12",
352
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
353
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
354
+ "cpu": [
355
+ "arm64"
356
+ ],
357
+ "dev": true,
358
+ "optional": true,
359
+ "os": [
360
+ "darwin"
361
+ ],
362
+ "engines": {
363
+ "node": ">=18"
364
+ }
365
+ },
366
+ "node_modules/@esbuild/darwin-x64": {
367
+ "version": "0.25.12",
368
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
369
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
370
+ "cpu": [
371
+ "x64"
372
+ ],
373
+ "dev": true,
374
+ "optional": true,
375
+ "os": [
376
+ "darwin"
377
+ ],
378
+ "engines": {
379
+ "node": ">=18"
380
+ }
381
+ },
382
+ "node_modules/@esbuild/freebsd-arm64": {
383
+ "version": "0.25.12",
384
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
385
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
386
+ "cpu": [
387
+ "arm64"
388
+ ],
389
+ "dev": true,
390
+ "optional": true,
391
+ "os": [
392
+ "freebsd"
393
+ ],
394
+ "engines": {
395
+ "node": ">=18"
396
+ }
397
+ },
398
+ "node_modules/@esbuild/freebsd-x64": {
399
+ "version": "0.25.12",
400
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
401
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
402
+ "cpu": [
403
+ "x64"
404
+ ],
405
+ "dev": true,
406
+ "optional": true,
407
+ "os": [
408
+ "freebsd"
409
+ ],
410
+ "engines": {
411
+ "node": ">=18"
412
+ }
413
+ },
414
+ "node_modules/@esbuild/linux-arm": {
415
+ "version": "0.25.12",
416
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
417
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
418
+ "cpu": [
419
+ "arm"
420
+ ],
421
+ "dev": true,
422
+ "optional": true,
423
+ "os": [
424
+ "linux"
425
+ ],
426
+ "engines": {
427
+ "node": ">=18"
428
+ }
429
+ },
430
+ "node_modules/@esbuild/linux-arm64": {
431
+ "version": "0.25.12",
432
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
433
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
434
+ "cpu": [
435
+ "arm64"
436
+ ],
437
+ "dev": true,
438
+ "optional": true,
439
+ "os": [
440
+ "linux"
441
+ ],
442
+ "engines": {
443
+ "node": ">=18"
444
+ }
445
+ },
446
+ "node_modules/@esbuild/linux-ia32": {
447
+ "version": "0.25.12",
448
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
449
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
450
+ "cpu": [
451
+ "ia32"
452
+ ],
453
+ "dev": true,
454
+ "optional": true,
455
+ "os": [
456
+ "linux"
457
+ ],
458
+ "engines": {
459
+ "node": ">=18"
460
+ }
461
+ },
462
+ "node_modules/@esbuild/linux-loong64": {
463
+ "version": "0.25.12",
464
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
465
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
466
+ "cpu": [
467
+ "loong64"
468
+ ],
469
+ "dev": true,
470
+ "optional": true,
471
+ "os": [
472
+ "linux"
473
+ ],
474
+ "engines": {
475
+ "node": ">=18"
476
+ }
477
+ },
478
+ "node_modules/@esbuild/linux-mips64el": {
479
+ "version": "0.25.12",
480
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
481
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
482
+ "cpu": [
483
+ "mips64el"
484
+ ],
485
+ "dev": true,
486
+ "optional": true,
487
+ "os": [
488
+ "linux"
489
+ ],
490
+ "engines": {
491
+ "node": ">=18"
492
+ }
493
+ },
494
+ "node_modules/@esbuild/linux-ppc64": {
495
+ "version": "0.25.12",
496
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
497
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
498
+ "cpu": [
499
+ "ppc64"
500
+ ],
501
+ "dev": true,
502
+ "optional": true,
503
+ "os": [
504
+ "linux"
505
+ ],
506
+ "engines": {
507
+ "node": ">=18"
508
+ }
509
+ },
510
+ "node_modules/@esbuild/linux-riscv64": {
511
+ "version": "0.25.12",
512
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
513
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
514
+ "cpu": [
515
+ "riscv64"
516
+ ],
517
+ "dev": true,
518
+ "optional": true,
519
+ "os": [
520
+ "linux"
521
+ ],
522
+ "engines": {
523
+ "node": ">=18"
524
+ }
525
+ },
526
+ "node_modules/@esbuild/linux-s390x": {
527
+ "version": "0.25.12",
528
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
529
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
530
+ "cpu": [
531
+ "s390x"
532
+ ],
533
+ "dev": true,
534
+ "optional": true,
535
+ "os": [
536
+ "linux"
537
+ ],
538
+ "engines": {
539
+ "node": ">=18"
540
+ }
541
+ },
542
+ "node_modules/@esbuild/linux-x64": {
543
+ "version": "0.25.12",
544
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
545
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
546
+ "cpu": [
547
+ "x64"
548
+ ],
549
+ "dev": true,
550
+ "optional": true,
551
+ "os": [
552
+ "linux"
553
+ ],
554
+ "engines": {
555
+ "node": ">=18"
556
+ }
557
+ },
558
+ "node_modules/@esbuild/netbsd-arm64": {
559
+ "version": "0.25.12",
560
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
561
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
562
+ "cpu": [
563
+ "arm64"
564
+ ],
565
+ "dev": true,
566
+ "optional": true,
567
+ "os": [
568
+ "netbsd"
569
+ ],
570
+ "engines": {
571
+ "node": ">=18"
572
+ }
573
+ },
574
+ "node_modules/@esbuild/netbsd-x64": {
575
+ "version": "0.25.12",
576
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
577
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
578
+ "cpu": [
579
+ "x64"
580
+ ],
581
+ "dev": true,
582
+ "optional": true,
583
+ "os": [
584
+ "netbsd"
585
+ ],
586
+ "engines": {
587
+ "node": ">=18"
588
+ }
589
+ },
590
+ "node_modules/@esbuild/openbsd-arm64": {
591
+ "version": "0.25.12",
592
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
593
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
594
+ "cpu": [
595
+ "arm64"
596
+ ],
597
+ "dev": true,
598
+ "optional": true,
599
+ "os": [
600
+ "openbsd"
601
+ ],
602
+ "engines": {
603
+ "node": ">=18"
604
+ }
605
+ },
606
+ "node_modules/@esbuild/openbsd-x64": {
607
+ "version": "0.25.12",
608
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
609
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
610
+ "cpu": [
611
+ "x64"
612
+ ],
613
+ "dev": true,
614
+ "optional": true,
615
+ "os": [
616
+ "openbsd"
617
+ ],
618
+ "engines": {
619
+ "node": ">=18"
620
+ }
621
+ },
622
+ "node_modules/@esbuild/openharmony-arm64": {
623
+ "version": "0.25.12",
624
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
625
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
626
+ "cpu": [
627
+ "arm64"
628
+ ],
629
+ "dev": true,
630
+ "optional": true,
631
+ "os": [
632
+ "openharmony"
633
+ ],
634
+ "engines": {
635
+ "node": ">=18"
636
+ }
637
+ },
638
+ "node_modules/@esbuild/sunos-x64": {
639
+ "version": "0.25.12",
640
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
641
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
642
+ "cpu": [
643
+ "x64"
644
+ ],
645
+ "dev": true,
646
+ "optional": true,
647
+ "os": [
648
+ "sunos"
649
+ ],
650
+ "engines": {
651
+ "node": ">=18"
652
+ }
653
+ },
654
+ "node_modules/@esbuild/win32-arm64": {
655
+ "version": "0.25.12",
656
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
657
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
658
+ "cpu": [
659
+ "arm64"
660
+ ],
661
+ "dev": true,
662
+ "optional": true,
663
+ "os": [
664
+ "win32"
665
+ ],
666
+ "engines": {
667
+ "node": ">=18"
668
+ }
669
+ },
670
+ "node_modules/@esbuild/win32-ia32": {
671
+ "version": "0.25.12",
672
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
673
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
674
+ "cpu": [
675
+ "ia32"
676
+ ],
677
+ "dev": true,
678
+ "optional": true,
679
+ "os": [
680
+ "win32"
681
+ ],
682
+ "engines": {
683
+ "node": ">=18"
684
+ }
685
+ },
686
+ "node_modules/@esbuild/win32-x64": {
687
+ "version": "0.25.12",
688
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
689
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
690
+ "cpu": [
691
+ "x64"
692
+ ],
693
+ "dev": true,
694
+ "optional": true,
695
+ "os": [
696
+ "win32"
697
+ ],
698
+ "engines": {
699
+ "node": ">=18"
700
+ }
701
+ },
702
+ "node_modules/@google/genai": {
703
+ "version": "1.30.0",
704
+ "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.30.0.tgz",
705
+ "integrity": "sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w==",
706
+ "dependencies": {
707
+ "google-auth-library": "^10.3.0",
708
+ "ws": "^8.18.0"
709
+ },
710
+ "engines": {
711
+ "node": ">=20.0.0"
712
+ },
713
+ "peerDependencies": {
714
+ "@modelcontextprotocol/sdk": "^1.20.1"
715
+ },
716
+ "peerDependenciesMeta": {
717
+ "@modelcontextprotocol/sdk": {
718
+ "optional": true
719
+ }
720
+ }
721
+ },
722
+ "node_modules/@isaacs/cliui": {
723
+ "version": "8.0.2",
724
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
725
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
726
+ "dependencies": {
727
+ "string-width": "^5.1.2",
728
+ "string-width-cjs": "npm:string-width@^4.2.0",
729
+ "strip-ansi": "^7.0.1",
730
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
731
+ "wrap-ansi": "^8.1.0",
732
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
733
+ },
734
+ "engines": {
735
+ "node": ">=12"
736
+ }
737
+ },
738
+ "node_modules/@jridgewell/gen-mapping": {
739
+ "version": "0.3.13",
740
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
741
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
742
+ "dev": true,
743
+ "dependencies": {
744
+ "@jridgewell/sourcemap-codec": "^1.5.0",
745
+ "@jridgewell/trace-mapping": "^0.3.24"
746
+ }
747
+ },
748
+ "node_modules/@jridgewell/remapping": {
749
+ "version": "2.3.5",
750
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
751
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
752
+ "dev": true,
753
+ "dependencies": {
754
+ "@jridgewell/gen-mapping": "^0.3.5",
755
+ "@jridgewell/trace-mapping": "^0.3.24"
756
+ }
757
+ },
758
+ "node_modules/@jridgewell/resolve-uri": {
759
+ "version": "3.1.2",
760
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
761
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
762
+ "dev": true,
763
+ "engines": {
764
+ "node": ">=6.0.0"
765
+ }
766
+ },
767
+ "node_modules/@jridgewell/sourcemap-codec": {
768
+ "version": "1.5.5",
769
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
770
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
771
+ "dev": true
772
+ },
773
+ "node_modules/@jridgewell/trace-mapping": {
774
+ "version": "0.3.31",
775
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
776
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
777
+ "dev": true,
778
+ "dependencies": {
779
+ "@jridgewell/resolve-uri": "^3.1.0",
780
+ "@jridgewell/sourcemap-codec": "^1.4.14"
781
+ }
782
+ },
783
+ "node_modules/@pkgjs/parseargs": {
784
+ "version": "0.11.0",
785
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
786
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
787
+ "optional": true,
788
+ "engines": {
789
+ "node": ">=14"
790
+ }
791
+ },
792
+ "node_modules/@rolldown/pluginutils": {
793
+ "version": "1.0.0-beta.47",
794
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz",
795
+ "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==",
796
+ "dev": true
797
+ },
798
+ "node_modules/@rollup/rollup-android-arm-eabi": {
799
+ "version": "4.53.3",
800
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
801
+ "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
802
+ "cpu": [
803
+ "arm"
804
+ ],
805
+ "dev": true,
806
+ "optional": true,
807
+ "os": [
808
+ "android"
809
+ ]
810
+ },
811
+ "node_modules/@rollup/rollup-android-arm64": {
812
+ "version": "4.53.3",
813
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
814
+ "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
815
+ "cpu": [
816
+ "arm64"
817
+ ],
818
+ "dev": true,
819
+ "optional": true,
820
+ "os": [
821
+ "android"
822
+ ]
823
+ },
824
+ "node_modules/@rollup/rollup-darwin-arm64": {
825
+ "version": "4.53.3",
826
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
827
+ "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
828
+ "cpu": [
829
+ "arm64"
830
+ ],
831
+ "dev": true,
832
+ "optional": true,
833
+ "os": [
834
+ "darwin"
835
+ ]
836
+ },
837
+ "node_modules/@rollup/rollup-darwin-x64": {
838
+ "version": "4.53.3",
839
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
840
+ "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
841
+ "cpu": [
842
+ "x64"
843
+ ],
844
+ "dev": true,
845
+ "optional": true,
846
+ "os": [
847
+ "darwin"
848
+ ]
849
+ },
850
+ "node_modules/@rollup/rollup-freebsd-arm64": {
851
+ "version": "4.53.3",
852
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
853
+ "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
854
+ "cpu": [
855
+ "arm64"
856
+ ],
857
+ "dev": true,
858
+ "optional": true,
859
+ "os": [
860
+ "freebsd"
861
+ ]
862
+ },
863
+ "node_modules/@rollup/rollup-freebsd-x64": {
864
+ "version": "4.53.3",
865
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
866
+ "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
867
+ "cpu": [
868
+ "x64"
869
+ ],
870
+ "dev": true,
871
+ "optional": true,
872
+ "os": [
873
+ "freebsd"
874
+ ]
875
+ },
876
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
877
+ "version": "4.53.3",
878
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
879
+ "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
880
+ "cpu": [
881
+ "arm"
882
+ ],
883
+ "dev": true,
884
+ "optional": true,
885
+ "os": [
886
+ "linux"
887
+ ]
888
+ },
889
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
890
+ "version": "4.53.3",
891
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
892
+ "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
893
+ "cpu": [
894
+ "arm"
895
+ ],
896
+ "dev": true,
897
+ "optional": true,
898
+ "os": [
899
+ "linux"
900
+ ]
901
+ },
902
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
903
+ "version": "4.53.3",
904
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
905
+ "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
906
+ "cpu": [
907
+ "arm64"
908
+ ],
909
+ "dev": true,
910
+ "optional": true,
911
+ "os": [
912
+ "linux"
913
+ ]
914
+ },
915
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
916
+ "version": "4.53.3",
917
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
918
+ "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
919
+ "cpu": [
920
+ "arm64"
921
+ ],
922
+ "dev": true,
923
+ "optional": true,
924
+ "os": [
925
+ "linux"
926
+ ]
927
+ },
928
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
929
+ "version": "4.53.3",
930
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
931
+ "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
932
+ "cpu": [
933
+ "loong64"
934
+ ],
935
+ "dev": true,
936
+ "optional": true,
937
+ "os": [
938
+ "linux"
939
+ ]
940
+ },
941
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
942
+ "version": "4.53.3",
943
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
944
+ "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
945
+ "cpu": [
946
+ "ppc64"
947
+ ],
948
+ "dev": true,
949
+ "optional": true,
950
+ "os": [
951
+ "linux"
952
+ ]
953
+ },
954
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
955
+ "version": "4.53.3",
956
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
957
+ "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
958
+ "cpu": [
959
+ "riscv64"
960
+ ],
961
+ "dev": true,
962
+ "optional": true,
963
+ "os": [
964
+ "linux"
965
+ ]
966
+ },
967
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
968
+ "version": "4.53.3",
969
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
970
+ "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
971
+ "cpu": [
972
+ "riscv64"
973
+ ],
974
+ "dev": true,
975
+ "optional": true,
976
+ "os": [
977
+ "linux"
978
+ ]
979
+ },
980
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
981
+ "version": "4.53.3",
982
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
983
+ "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
984
+ "cpu": [
985
+ "s390x"
986
+ ],
987
+ "dev": true,
988
+ "optional": true,
989
+ "os": [
990
+ "linux"
991
+ ]
992
+ },
993
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
994
+ "version": "4.53.3",
995
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
996
+ "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
997
+ "cpu": [
998
+ "x64"
999
+ ],
1000
+ "dev": true,
1001
+ "optional": true,
1002
+ "os": [
1003
+ "linux"
1004
+ ]
1005
+ },
1006
+ "node_modules/@rollup/rollup-linux-x64-musl": {
1007
+ "version": "4.53.3",
1008
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
1009
+ "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
1010
+ "cpu": [
1011
+ "x64"
1012
+ ],
1013
+ "dev": true,
1014
+ "optional": true,
1015
+ "os": [
1016
+ "linux"
1017
+ ]
1018
+ },
1019
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1020
+ "version": "4.53.3",
1021
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
1022
+ "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
1023
+ "cpu": [
1024
+ "arm64"
1025
+ ],
1026
+ "dev": true,
1027
+ "optional": true,
1028
+ "os": [
1029
+ "openharmony"
1030
+ ]
1031
+ },
1032
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1033
+ "version": "4.53.3",
1034
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
1035
+ "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
1036
+ "cpu": [
1037
+ "arm64"
1038
+ ],
1039
+ "dev": true,
1040
+ "optional": true,
1041
+ "os": [
1042
+ "win32"
1043
+ ]
1044
+ },
1045
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1046
+ "version": "4.53.3",
1047
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
1048
+ "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
1049
+ "cpu": [
1050
+ "ia32"
1051
+ ],
1052
+ "dev": true,
1053
+ "optional": true,
1054
+ "os": [
1055
+ "win32"
1056
+ ]
1057
+ },
1058
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1059
+ "version": "4.53.3",
1060
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
1061
+ "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
1062
+ "cpu": [
1063
+ "x64"
1064
+ ],
1065
+ "dev": true,
1066
+ "optional": true,
1067
+ "os": [
1068
+ "win32"
1069
+ ]
1070
+ },
1071
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1072
+ "version": "4.53.3",
1073
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
1074
+ "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
1075
+ "cpu": [
1076
+ "x64"
1077
+ ],
1078
+ "dev": true,
1079
+ "optional": true,
1080
+ "os": [
1081
+ "win32"
1082
+ ]
1083
+ },
1084
+ "node_modules/@types/babel__core": {
1085
+ "version": "7.20.5",
1086
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1087
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1088
+ "dev": true,
1089
+ "dependencies": {
1090
+ "@babel/parser": "^7.20.7",
1091
+ "@babel/types": "^7.20.7",
1092
+ "@types/babel__generator": "*",
1093
+ "@types/babel__template": "*",
1094
+ "@types/babel__traverse": "*"
1095
+ }
1096
+ },
1097
+ "node_modules/@types/babel__generator": {
1098
+ "version": "7.27.0",
1099
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1100
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1101
+ "dev": true,
1102
+ "dependencies": {
1103
+ "@babel/types": "^7.0.0"
1104
+ }
1105
+ },
1106
+ "node_modules/@types/babel__template": {
1107
+ "version": "7.4.4",
1108
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1109
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1110
+ "dev": true,
1111
+ "dependencies": {
1112
+ "@babel/parser": "^7.1.0",
1113
+ "@babel/types": "^7.0.0"
1114
+ }
1115
+ },
1116
+ "node_modules/@types/babel__traverse": {
1117
+ "version": "7.28.0",
1118
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1119
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1120
+ "dev": true,
1121
+ "dependencies": {
1122
+ "@babel/types": "^7.28.2"
1123
+ }
1124
+ },
1125
+ "node_modules/@types/estree": {
1126
+ "version": "1.0.8",
1127
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
1128
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
1129
+ "dev": true
1130
+ },
1131
+ "node_modules/@types/node": {
1132
+ "version": "22.19.1",
1133
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
1134
+ "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
1135
+ "dev": true,
1136
+ "dependencies": {
1137
+ "undici-types": "~6.21.0"
1138
+ }
1139
+ },
1140
+ "node_modules/@vitejs/plugin-react": {
1141
+ "version": "5.1.1",
1142
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz",
1143
+ "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==",
1144
+ "dev": true,
1145
+ "dependencies": {
1146
+ "@babel/core": "^7.28.5",
1147
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1148
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1149
+ "@rolldown/pluginutils": "1.0.0-beta.47",
1150
+ "@types/babel__core": "^7.20.5",
1151
+ "react-refresh": "^0.18.0"
1152
+ },
1153
+ "engines": {
1154
+ "node": "^20.19.0 || >=22.12.0"
1155
+ },
1156
+ "peerDependencies": {
1157
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1158
+ }
1159
+ },
1160
+ "node_modules/agent-base": {
1161
+ "version": "7.1.4",
1162
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
1163
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
1164
+ "engines": {
1165
+ "node": ">= 14"
1166
+ }
1167
+ },
1168
+ "node_modules/ansi-regex": {
1169
+ "version": "6.2.2",
1170
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
1171
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
1172
+ "engines": {
1173
+ "node": ">=12"
1174
+ },
1175
+ "funding": {
1176
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
1177
+ }
1178
+ },
1179
+ "node_modules/ansi-styles": {
1180
+ "version": "6.2.3",
1181
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
1182
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
1183
+ "engines": {
1184
+ "node": ">=12"
1185
+ },
1186
+ "funding": {
1187
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
1188
+ }
1189
+ },
1190
+ "node_modules/balanced-match": {
1191
+ "version": "1.0.2",
1192
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
1193
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
1194
+ },
1195
+ "node_modules/base64-js": {
1196
+ "version": "1.5.1",
1197
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
1198
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
1199
+ "funding": [
1200
+ {
1201
+ "type": "github",
1202
+ "url": "https://github.com/sponsors/feross"
1203
+ },
1204
+ {
1205
+ "type": "patreon",
1206
+ "url": "https://www.patreon.com/feross"
1207
+ },
1208
+ {
1209
+ "type": "consulting",
1210
+ "url": "https://feross.org/support"
1211
+ }
1212
+ ]
1213
+ },
1214
+ "node_modules/baseline-browser-mapping": {
1215
+ "version": "2.8.30",
1216
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz",
1217
+ "integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==",
1218
+ "dev": true,
1219
+ "bin": {
1220
+ "baseline-browser-mapping": "dist/cli.js"
1221
+ }
1222
+ },
1223
+ "node_modules/bignumber.js": {
1224
+ "version": "9.3.1",
1225
+ "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
1226
+ "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
1227
+ "engines": {
1228
+ "node": "*"
1229
+ }
1230
+ },
1231
+ "node_modules/brace-expansion": {
1232
+ "version": "2.0.2",
1233
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
1234
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
1235
+ "dependencies": {
1236
+ "balanced-match": "^1.0.0"
1237
+ }
1238
+ },
1239
+ "node_modules/browserslist": {
1240
+ "version": "4.28.0",
1241
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
1242
+ "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==",
1243
+ "dev": true,
1244
+ "funding": [
1245
+ {
1246
+ "type": "opencollective",
1247
+ "url": "https://opencollective.com/browserslist"
1248
+ },
1249
+ {
1250
+ "type": "tidelift",
1251
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1252
+ },
1253
+ {
1254
+ "type": "github",
1255
+ "url": "https://github.com/sponsors/ai"
1256
+ }
1257
+ ],
1258
+ "dependencies": {
1259
+ "baseline-browser-mapping": "^2.8.25",
1260
+ "caniuse-lite": "^1.0.30001754",
1261
+ "electron-to-chromium": "^1.5.249",
1262
+ "node-releases": "^2.0.27",
1263
+ "update-browserslist-db": "^1.1.4"
1264
+ },
1265
+ "bin": {
1266
+ "browserslist": "cli.js"
1267
+ },
1268
+ "engines": {
1269
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1270
+ }
1271
+ },
1272
+ "node_modules/buffer-equal-constant-time": {
1273
+ "version": "1.0.1",
1274
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
1275
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
1276
+ },
1277
+ "node_modules/caniuse-lite": {
1278
+ "version": "1.0.30001756",
1279
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz",
1280
+ "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==",
1281
+ "dev": true,
1282
+ "funding": [
1283
+ {
1284
+ "type": "opencollective",
1285
+ "url": "https://opencollective.com/browserslist"
1286
+ },
1287
+ {
1288
+ "type": "tidelift",
1289
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1290
+ },
1291
+ {
1292
+ "type": "github",
1293
+ "url": "https://github.com/sponsors/ai"
1294
+ }
1295
+ ]
1296
+ },
1297
+ "node_modules/color-convert": {
1298
+ "version": "2.0.1",
1299
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
1300
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
1301
+ "dependencies": {
1302
+ "color-name": "~1.1.4"
1303
+ },
1304
+ "engines": {
1305
+ "node": ">=7.0.0"
1306
+ }
1307
+ },
1308
+ "node_modules/color-name": {
1309
+ "version": "1.1.4",
1310
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
1311
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
1312
+ },
1313
+ "node_modules/convert-source-map": {
1314
+ "version": "2.0.0",
1315
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1316
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1317
+ "dev": true
1318
+ },
1319
+ "node_modules/cookie": {
1320
+ "version": "1.0.2",
1321
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
1322
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
1323
+ "engines": {
1324
+ "node": ">=18"
1325
+ }
1326
+ },
1327
+ "node_modules/cross-spawn": {
1328
+ "version": "7.0.6",
1329
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
1330
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
1331
+ "dependencies": {
1332
+ "path-key": "^3.1.0",
1333
+ "shebang-command": "^2.0.0",
1334
+ "which": "^2.0.1"
1335
+ },
1336
+ "engines": {
1337
+ "node": ">= 8"
1338
+ }
1339
+ },
1340
+ "node_modules/data-uri-to-buffer": {
1341
+ "version": "4.0.1",
1342
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
1343
+ "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
1344
+ "engines": {
1345
+ "node": ">= 12"
1346
+ }
1347
+ },
1348
+ "node_modules/debug": {
1349
+ "version": "4.4.3",
1350
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1351
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1352
+ "dependencies": {
1353
+ "ms": "^2.1.3"
1354
+ },
1355
+ "engines": {
1356
+ "node": ">=6.0"
1357
+ },
1358
+ "peerDependenciesMeta": {
1359
+ "supports-color": {
1360
+ "optional": true
1361
+ }
1362
+ }
1363
+ },
1364
+ "node_modules/eastasianwidth": {
1365
+ "version": "0.2.0",
1366
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
1367
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
1368
+ },
1369
+ "node_modules/ecdsa-sig-formatter": {
1370
+ "version": "1.0.11",
1371
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
1372
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
1373
+ "dependencies": {
1374
+ "safe-buffer": "^5.0.1"
1375
+ }
1376
+ },
1377
+ "node_modules/electron-to-chromium": {
1378
+ "version": "1.5.259",
1379
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz",
1380
+ "integrity": "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==",
1381
+ "dev": true
1382
+ },
1383
+ "node_modules/emoji-regex": {
1384
+ "version": "9.2.2",
1385
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
1386
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
1387
+ },
1388
+ "node_modules/esbuild": {
1389
+ "version": "0.25.12",
1390
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
1391
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
1392
+ "dev": true,
1393
+ "hasInstallScript": true,
1394
+ "bin": {
1395
+ "esbuild": "bin/esbuild"
1396
+ },
1397
+ "engines": {
1398
+ "node": ">=18"
1399
+ },
1400
+ "optionalDependencies": {
1401
+ "@esbuild/aix-ppc64": "0.25.12",
1402
+ "@esbuild/android-arm": "0.25.12",
1403
+ "@esbuild/android-arm64": "0.25.12",
1404
+ "@esbuild/android-x64": "0.25.12",
1405
+ "@esbuild/darwin-arm64": "0.25.12",
1406
+ "@esbuild/darwin-x64": "0.25.12",
1407
+ "@esbuild/freebsd-arm64": "0.25.12",
1408
+ "@esbuild/freebsd-x64": "0.25.12",
1409
+ "@esbuild/linux-arm": "0.25.12",
1410
+ "@esbuild/linux-arm64": "0.25.12",
1411
+ "@esbuild/linux-ia32": "0.25.12",
1412
+ "@esbuild/linux-loong64": "0.25.12",
1413
+ "@esbuild/linux-mips64el": "0.25.12",
1414
+ "@esbuild/linux-ppc64": "0.25.12",
1415
+ "@esbuild/linux-riscv64": "0.25.12",
1416
+ "@esbuild/linux-s390x": "0.25.12",
1417
+ "@esbuild/linux-x64": "0.25.12",
1418
+ "@esbuild/netbsd-arm64": "0.25.12",
1419
+ "@esbuild/netbsd-x64": "0.25.12",
1420
+ "@esbuild/openbsd-arm64": "0.25.12",
1421
+ "@esbuild/openbsd-x64": "0.25.12",
1422
+ "@esbuild/openharmony-arm64": "0.25.12",
1423
+ "@esbuild/sunos-x64": "0.25.12",
1424
+ "@esbuild/win32-arm64": "0.25.12",
1425
+ "@esbuild/win32-ia32": "0.25.12",
1426
+ "@esbuild/win32-x64": "0.25.12"
1427
+ }
1428
+ },
1429
+ "node_modules/escalade": {
1430
+ "version": "3.2.0",
1431
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1432
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1433
+ "dev": true,
1434
+ "engines": {
1435
+ "node": ">=6"
1436
+ }
1437
+ },
1438
+ "node_modules/extend": {
1439
+ "version": "3.0.2",
1440
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
1441
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
1442
+ },
1443
+ "node_modules/fdir": {
1444
+ "version": "6.5.0",
1445
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1446
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1447
+ "dev": true,
1448
+ "engines": {
1449
+ "node": ">=12.0.0"
1450
+ },
1451
+ "peerDependencies": {
1452
+ "picomatch": "^3 || ^4"
1453
+ },
1454
+ "peerDependenciesMeta": {
1455
+ "picomatch": {
1456
+ "optional": true
1457
+ }
1458
+ }
1459
+ },
1460
+ "node_modules/fetch-blob": {
1461
+ "version": "3.2.0",
1462
+ "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
1463
+ "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
1464
+ "funding": [
1465
+ {
1466
+ "type": "github",
1467
+ "url": "https://github.com/sponsors/jimmywarting"
1468
+ },
1469
+ {
1470
+ "type": "paypal",
1471
+ "url": "https://paypal.me/jimmywarting"
1472
+ }
1473
+ ],
1474
+ "dependencies": {
1475
+ "node-domexception": "^1.0.0",
1476
+ "web-streams-polyfill": "^3.0.3"
1477
+ },
1478
+ "engines": {
1479
+ "node": "^12.20 || >= 14.13"
1480
+ }
1481
+ },
1482
+ "node_modules/foreground-child": {
1483
+ "version": "3.3.1",
1484
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
1485
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
1486
+ "dependencies": {
1487
+ "cross-spawn": "^7.0.6",
1488
+ "signal-exit": "^4.0.1"
1489
+ },
1490
+ "engines": {
1491
+ "node": ">=14"
1492
+ },
1493
+ "funding": {
1494
+ "url": "https://github.com/sponsors/isaacs"
1495
+ }
1496
+ },
1497
+ "node_modules/formdata-polyfill": {
1498
+ "version": "4.0.10",
1499
+ "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
1500
+ "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
1501
+ "dependencies": {
1502
+ "fetch-blob": "^3.1.2"
1503
+ },
1504
+ "engines": {
1505
+ "node": ">=12.20.0"
1506
+ }
1507
+ },
1508
+ "node_modules/fsevents": {
1509
+ "version": "2.3.3",
1510
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1511
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1512
+ "dev": true,
1513
+ "hasInstallScript": true,
1514
+ "optional": true,
1515
+ "os": [
1516
+ "darwin"
1517
+ ],
1518
+ "engines": {
1519
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1520
+ }
1521
+ },
1522
+ "node_modules/gaxios": {
1523
+ "version": "7.1.3",
1524
+ "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
1525
+ "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
1526
+ "dependencies": {
1527
+ "extend": "^3.0.2",
1528
+ "https-proxy-agent": "^7.0.1",
1529
+ "node-fetch": "^3.3.2",
1530
+ "rimraf": "^5.0.1"
1531
+ },
1532
+ "engines": {
1533
+ "node": ">=18"
1534
+ }
1535
+ },
1536
+ "node_modules/gcp-metadata": {
1537
+ "version": "8.1.2",
1538
+ "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
1539
+ "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
1540
+ "dependencies": {
1541
+ "gaxios": "^7.0.0",
1542
+ "google-logging-utils": "^1.0.0",
1543
+ "json-bigint": "^1.0.0"
1544
+ },
1545
+ "engines": {
1546
+ "node": ">=18"
1547
+ }
1548
+ },
1549
+ "node_modules/gensync": {
1550
+ "version": "1.0.0-beta.2",
1551
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1552
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1553
+ "dev": true,
1554
+ "engines": {
1555
+ "node": ">=6.9.0"
1556
+ }
1557
+ },
1558
+ "node_modules/glob": {
1559
+ "version": "10.5.0",
1560
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
1561
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
1562
+ "dependencies": {
1563
+ "foreground-child": "^3.1.0",
1564
+ "jackspeak": "^3.1.2",
1565
+ "minimatch": "^9.0.4",
1566
+ "minipass": "^7.1.2",
1567
+ "package-json-from-dist": "^1.0.0",
1568
+ "path-scurry": "^1.11.1"
1569
+ },
1570
+ "bin": {
1571
+ "glob": "dist/esm/bin.mjs"
1572
+ },
1573
+ "funding": {
1574
+ "url": "https://github.com/sponsors/isaacs"
1575
+ }
1576
+ },
1577
+ "node_modules/google-auth-library": {
1578
+ "version": "10.5.0",
1579
+ "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz",
1580
+ "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==",
1581
+ "dependencies": {
1582
+ "base64-js": "^1.3.0",
1583
+ "ecdsa-sig-formatter": "^1.0.11",
1584
+ "gaxios": "^7.0.0",
1585
+ "gcp-metadata": "^8.0.0",
1586
+ "google-logging-utils": "^1.0.0",
1587
+ "gtoken": "^8.0.0",
1588
+ "jws": "^4.0.0"
1589
+ },
1590
+ "engines": {
1591
+ "node": ">=18"
1592
+ }
1593
+ },
1594
+ "node_modules/google-logging-utils": {
1595
+ "version": "1.1.3",
1596
+ "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
1597
+ "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
1598
+ "engines": {
1599
+ "node": ">=14"
1600
+ }
1601
+ },
1602
+ "node_modules/gtoken": {
1603
+ "version": "8.0.0",
1604
+ "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
1605
+ "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
1606
+ "dependencies": {
1607
+ "gaxios": "^7.0.0",
1608
+ "jws": "^4.0.0"
1609
+ },
1610
+ "engines": {
1611
+ "node": ">=18"
1612
+ }
1613
+ },
1614
+ "node_modules/https-proxy-agent": {
1615
+ "version": "7.0.6",
1616
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
1617
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
1618
+ "dependencies": {
1619
+ "agent-base": "^7.1.2",
1620
+ "debug": "4"
1621
+ },
1622
+ "engines": {
1623
+ "node": ">= 14"
1624
+ }
1625
+ },
1626
+ "node_modules/is-fullwidth-code-point": {
1627
+ "version": "3.0.0",
1628
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
1629
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
1630
+ "engines": {
1631
+ "node": ">=8"
1632
+ }
1633
+ },
1634
+ "node_modules/isexe": {
1635
+ "version": "2.0.0",
1636
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
1637
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
1638
+ },
1639
+ "node_modules/jackspeak": {
1640
+ "version": "3.4.3",
1641
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
1642
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
1643
+ "dependencies": {
1644
+ "@isaacs/cliui": "^8.0.2"
1645
+ },
1646
+ "funding": {
1647
+ "url": "https://github.com/sponsors/isaacs"
1648
+ },
1649
+ "optionalDependencies": {
1650
+ "@pkgjs/parseargs": "^0.11.0"
1651
+ }
1652
+ },
1653
+ "node_modules/js-tokens": {
1654
+ "version": "4.0.0",
1655
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1656
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1657
+ "dev": true
1658
+ },
1659
+ "node_modules/jsesc": {
1660
+ "version": "3.1.0",
1661
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1662
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1663
+ "dev": true,
1664
+ "bin": {
1665
+ "jsesc": "bin/jsesc"
1666
+ },
1667
+ "engines": {
1668
+ "node": ">=6"
1669
+ }
1670
+ },
1671
+ "node_modules/json-bigint": {
1672
+ "version": "1.0.0",
1673
+ "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
1674
+ "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
1675
+ "dependencies": {
1676
+ "bignumber.js": "^9.0.0"
1677
+ }
1678
+ },
1679
+ "node_modules/json5": {
1680
+ "version": "2.2.3",
1681
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1682
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1683
+ "dev": true,
1684
+ "bin": {
1685
+ "json5": "lib/cli.js"
1686
+ },
1687
+ "engines": {
1688
+ "node": ">=6"
1689
+ }
1690
+ },
1691
+ "node_modules/jwa": {
1692
+ "version": "2.0.1",
1693
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
1694
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
1695
+ "dependencies": {
1696
+ "buffer-equal-constant-time": "^1.0.1",
1697
+ "ecdsa-sig-formatter": "1.0.11",
1698
+ "safe-buffer": "^5.0.1"
1699
+ }
1700
+ },
1701
+ "node_modules/jws": {
1702
+ "version": "4.0.0",
1703
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
1704
+ "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
1705
+ "dependencies": {
1706
+ "jwa": "^2.0.0",
1707
+ "safe-buffer": "^5.0.1"
1708
+ }
1709
+ },
1710
+ "node_modules/lru-cache": {
1711
+ "version": "5.1.1",
1712
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1713
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1714
+ "dev": true,
1715
+ "dependencies": {
1716
+ "yallist": "^3.0.2"
1717
+ }
1718
+ },
1719
+ "node_modules/minimatch": {
1720
+ "version": "9.0.5",
1721
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
1722
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
1723
+ "dependencies": {
1724
+ "brace-expansion": "^2.0.1"
1725
+ },
1726
+ "engines": {
1727
+ "node": ">=16 || 14 >=14.17"
1728
+ },
1729
+ "funding": {
1730
+ "url": "https://github.com/sponsors/isaacs"
1731
+ }
1732
+ },
1733
+ "node_modules/minipass": {
1734
+ "version": "7.1.2",
1735
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
1736
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
1737
+ "engines": {
1738
+ "node": ">=16 || 14 >=14.17"
1739
+ }
1740
+ },
1741
+ "node_modules/ms": {
1742
+ "version": "2.1.3",
1743
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1744
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1745
+ },
1746
+ "node_modules/nanoid": {
1747
+ "version": "3.3.11",
1748
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1749
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1750
+ "dev": true,
1751
+ "funding": [
1752
+ {
1753
+ "type": "github",
1754
+ "url": "https://github.com/sponsors/ai"
1755
+ }
1756
+ ],
1757
+ "bin": {
1758
+ "nanoid": "bin/nanoid.cjs"
1759
+ },
1760
+ "engines": {
1761
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1762
+ }
1763
+ },
1764
+ "node_modules/node-domexception": {
1765
+ "version": "1.0.0",
1766
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
1767
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
1768
+ "deprecated": "Use your platform's native DOMException instead",
1769
+ "funding": [
1770
+ {
1771
+ "type": "github",
1772
+ "url": "https://github.com/sponsors/jimmywarting"
1773
+ },
1774
+ {
1775
+ "type": "github",
1776
+ "url": "https://paypal.me/jimmywarting"
1777
+ }
1778
+ ],
1779
+ "engines": {
1780
+ "node": ">=10.5.0"
1781
+ }
1782
+ },
1783
+ "node_modules/node-fetch": {
1784
+ "version": "3.3.2",
1785
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
1786
+ "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
1787
+ "dependencies": {
1788
+ "data-uri-to-buffer": "^4.0.0",
1789
+ "fetch-blob": "^3.1.4",
1790
+ "formdata-polyfill": "^4.0.10"
1791
+ },
1792
+ "engines": {
1793
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
1794
+ },
1795
+ "funding": {
1796
+ "type": "opencollective",
1797
+ "url": "https://opencollective.com/node-fetch"
1798
+ }
1799
+ },
1800
+ "node_modules/node-releases": {
1801
+ "version": "2.0.27",
1802
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
1803
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
1804
+ "dev": true
1805
+ },
1806
+ "node_modules/package-json-from-dist": {
1807
+ "version": "1.0.1",
1808
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
1809
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
1810
+ },
1811
+ "node_modules/path-key": {
1812
+ "version": "3.1.1",
1813
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
1814
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
1815
+ "engines": {
1816
+ "node": ">=8"
1817
+ }
1818
+ },
1819
+ "node_modules/path-scurry": {
1820
+ "version": "1.11.1",
1821
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
1822
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
1823
+ "dependencies": {
1824
+ "lru-cache": "^10.2.0",
1825
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
1826
+ },
1827
+ "engines": {
1828
+ "node": ">=16 || 14 >=14.18"
1829
+ },
1830
+ "funding": {
1831
+ "url": "https://github.com/sponsors/isaacs"
1832
+ }
1833
+ },
1834
+ "node_modules/path-scurry/node_modules/lru-cache": {
1835
+ "version": "10.4.3",
1836
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
1837
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
1838
+ },
1839
+ "node_modules/picocolors": {
1840
+ "version": "1.1.1",
1841
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1842
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1843
+ "dev": true
1844
+ },
1845
+ "node_modules/picomatch": {
1846
+ "version": "4.0.3",
1847
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
1848
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1849
+ "dev": true,
1850
+ "engines": {
1851
+ "node": ">=12"
1852
+ },
1853
+ "funding": {
1854
+ "url": "https://github.com/sponsors/jonschlinkert"
1855
+ }
1856
+ },
1857
+ "node_modules/postcss": {
1858
+ "version": "8.5.6",
1859
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
1860
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
1861
+ "dev": true,
1862
+ "funding": [
1863
+ {
1864
+ "type": "opencollective",
1865
+ "url": "https://opencollective.com/postcss/"
1866
+ },
1867
+ {
1868
+ "type": "tidelift",
1869
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1870
+ },
1871
+ {
1872
+ "type": "github",
1873
+ "url": "https://github.com/sponsors/ai"
1874
+ }
1875
+ ],
1876
+ "dependencies": {
1877
+ "nanoid": "^3.3.11",
1878
+ "picocolors": "^1.1.1",
1879
+ "source-map-js": "^1.2.1"
1880
+ },
1881
+ "engines": {
1882
+ "node": "^10 || ^12 || >=14"
1883
+ }
1884
+ },
1885
+ "node_modules/react": {
1886
+ "version": "19.2.0",
1887
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
1888
+ "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
1889
+ "engines": {
1890
+ "node": ">=0.10.0"
1891
+ }
1892
+ },
1893
+ "node_modules/react-dom": {
1894
+ "version": "19.2.0",
1895
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
1896
+ "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
1897
+ "dependencies": {
1898
+ "scheduler": "^0.27.0"
1899
+ },
1900
+ "peerDependencies": {
1901
+ "react": "^19.2.0"
1902
+ }
1903
+ },
1904
+ "node_modules/react-refresh": {
1905
+ "version": "0.18.0",
1906
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
1907
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
1908
+ "dev": true,
1909
+ "engines": {
1910
+ "node": ">=0.10.0"
1911
+ }
1912
+ },
1913
+ "node_modules/react-router": {
1914
+ "version": "7.9.6",
1915
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz",
1916
+ "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==",
1917
+ "dependencies": {
1918
+ "cookie": "^1.0.1",
1919
+ "set-cookie-parser": "^2.6.0"
1920
+ },
1921
+ "engines": {
1922
+ "node": ">=20.0.0"
1923
+ },
1924
+ "peerDependencies": {
1925
+ "react": ">=18",
1926
+ "react-dom": ">=18"
1927
+ },
1928
+ "peerDependenciesMeta": {
1929
+ "react-dom": {
1930
+ "optional": true
1931
+ }
1932
+ }
1933
+ },
1934
+ "node_modules/react-router-dom": {
1935
+ "version": "7.9.6",
1936
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz",
1937
+ "integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==",
1938
+ "dependencies": {
1939
+ "react-router": "7.9.6"
1940
+ },
1941
+ "engines": {
1942
+ "node": ">=20.0.0"
1943
+ },
1944
+ "peerDependencies": {
1945
+ "react": ">=18",
1946
+ "react-dom": ">=18"
1947
+ }
1948
+ },
1949
+ "node_modules/rimraf": {
1950
+ "version": "5.0.10",
1951
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
1952
+ "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
1953
+ "dependencies": {
1954
+ "glob": "^10.3.7"
1955
+ },
1956
+ "bin": {
1957
+ "rimraf": "dist/esm/bin.mjs"
1958
+ },
1959
+ "funding": {
1960
+ "url": "https://github.com/sponsors/isaacs"
1961
+ }
1962
+ },
1963
+ "node_modules/rollup": {
1964
+ "version": "4.53.3",
1965
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
1966
+ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
1967
+ "dev": true,
1968
+ "dependencies": {
1969
+ "@types/estree": "1.0.8"
1970
+ },
1971
+ "bin": {
1972
+ "rollup": "dist/bin/rollup"
1973
+ },
1974
+ "engines": {
1975
+ "node": ">=18.0.0",
1976
+ "npm": ">=8.0.0"
1977
+ },
1978
+ "optionalDependencies": {
1979
+ "@rollup/rollup-android-arm-eabi": "4.53.3",
1980
+ "@rollup/rollup-android-arm64": "4.53.3",
1981
+ "@rollup/rollup-darwin-arm64": "4.53.3",
1982
+ "@rollup/rollup-darwin-x64": "4.53.3",
1983
+ "@rollup/rollup-freebsd-arm64": "4.53.3",
1984
+ "@rollup/rollup-freebsd-x64": "4.53.3",
1985
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
1986
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.3",
1987
+ "@rollup/rollup-linux-arm64-gnu": "4.53.3",
1988
+ "@rollup/rollup-linux-arm64-musl": "4.53.3",
1989
+ "@rollup/rollup-linux-loong64-gnu": "4.53.3",
1990
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.3",
1991
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.3",
1992
+ "@rollup/rollup-linux-riscv64-musl": "4.53.3",
1993
+ "@rollup/rollup-linux-s390x-gnu": "4.53.3",
1994
+ "@rollup/rollup-linux-x64-gnu": "4.53.3",
1995
+ "@rollup/rollup-linux-x64-musl": "4.53.3",
1996
+ "@rollup/rollup-openharmony-arm64": "4.53.3",
1997
+ "@rollup/rollup-win32-arm64-msvc": "4.53.3",
1998
+ "@rollup/rollup-win32-ia32-msvc": "4.53.3",
1999
+ "@rollup/rollup-win32-x64-gnu": "4.53.3",
2000
+ "@rollup/rollup-win32-x64-msvc": "4.53.3",
2001
+ "fsevents": "~2.3.2"
2002
+ }
2003
+ },
2004
+ "node_modules/safe-buffer": {
2005
+ "version": "5.2.1",
2006
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
2007
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
2008
+ "funding": [
2009
+ {
2010
+ "type": "github",
2011
+ "url": "https://github.com/sponsors/feross"
2012
+ },
2013
+ {
2014
+ "type": "patreon",
2015
+ "url": "https://www.patreon.com/feross"
2016
+ },
2017
+ {
2018
+ "type": "consulting",
2019
+ "url": "https://feross.org/support"
2020
+ }
2021
+ ]
2022
+ },
2023
+ "node_modules/scheduler": {
2024
+ "version": "0.27.0",
2025
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
2026
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="
2027
+ },
2028
+ "node_modules/semver": {
2029
+ "version": "6.3.1",
2030
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
2031
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
2032
+ "dev": true,
2033
+ "bin": {
2034
+ "semver": "bin/semver.js"
2035
+ }
2036
+ },
2037
+ "node_modules/set-cookie-parser": {
2038
+ "version": "2.7.2",
2039
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
2040
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="
2041
+ },
2042
+ "node_modules/shebang-command": {
2043
+ "version": "2.0.0",
2044
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
2045
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
2046
+ "dependencies": {
2047
+ "shebang-regex": "^3.0.0"
2048
+ },
2049
+ "engines": {
2050
+ "node": ">=8"
2051
+ }
2052
+ },
2053
+ "node_modules/shebang-regex": {
2054
+ "version": "3.0.0",
2055
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
2056
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
2057
+ "engines": {
2058
+ "node": ">=8"
2059
+ }
2060
+ },
2061
+ "node_modules/signal-exit": {
2062
+ "version": "4.1.0",
2063
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
2064
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
2065
+ "engines": {
2066
+ "node": ">=14"
2067
+ },
2068
+ "funding": {
2069
+ "url": "https://github.com/sponsors/isaacs"
2070
+ }
2071
+ },
2072
+ "node_modules/source-map-js": {
2073
+ "version": "1.2.1",
2074
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
2075
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
2076
+ "dev": true,
2077
+ "engines": {
2078
+ "node": ">=0.10.0"
2079
+ }
2080
+ },
2081
+ "node_modules/string-width": {
2082
+ "version": "5.1.2",
2083
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
2084
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
2085
+ "dependencies": {
2086
+ "eastasianwidth": "^0.2.0",
2087
+ "emoji-regex": "^9.2.2",
2088
+ "strip-ansi": "^7.0.1"
2089
+ },
2090
+ "engines": {
2091
+ "node": ">=12"
2092
+ },
2093
+ "funding": {
2094
+ "url": "https://github.com/sponsors/sindresorhus"
2095
+ }
2096
+ },
2097
+ "node_modules/string-width-cjs": {
2098
+ "name": "string-width",
2099
+ "version": "4.2.3",
2100
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
2101
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
2102
+ "dependencies": {
2103
+ "emoji-regex": "^8.0.0",
2104
+ "is-fullwidth-code-point": "^3.0.0",
2105
+ "strip-ansi": "^6.0.1"
2106
+ },
2107
+ "engines": {
2108
+ "node": ">=8"
2109
+ }
2110
+ },
2111
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
2112
+ "version": "5.0.1",
2113
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
2114
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
2115
+ "engines": {
2116
+ "node": ">=8"
2117
+ }
2118
+ },
2119
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
2120
+ "version": "8.0.0",
2121
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
2122
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
2123
+ },
2124
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
2125
+ "version": "6.0.1",
2126
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2127
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2128
+ "dependencies": {
2129
+ "ansi-regex": "^5.0.1"
2130
+ },
2131
+ "engines": {
2132
+ "node": ">=8"
2133
+ }
2134
+ },
2135
+ "node_modules/strip-ansi": {
2136
+ "version": "7.1.2",
2137
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
2138
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
2139
+ "dependencies": {
2140
+ "ansi-regex": "^6.0.1"
2141
+ },
2142
+ "engines": {
2143
+ "node": ">=12"
2144
+ },
2145
+ "funding": {
2146
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
2147
+ }
2148
+ },
2149
+ "node_modules/strip-ansi-cjs": {
2150
+ "name": "strip-ansi",
2151
+ "version": "6.0.1",
2152
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2153
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2154
+ "dependencies": {
2155
+ "ansi-regex": "^5.0.1"
2156
+ },
2157
+ "engines": {
2158
+ "node": ">=8"
2159
+ }
2160
+ },
2161
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
2162
+ "version": "5.0.1",
2163
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
2164
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
2165
+ "engines": {
2166
+ "node": ">=8"
2167
+ }
2168
+ },
2169
+ "node_modules/tinyglobby": {
2170
+ "version": "0.2.15",
2171
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
2172
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
2173
+ "dev": true,
2174
+ "dependencies": {
2175
+ "fdir": "^6.5.0",
2176
+ "picomatch": "^4.0.3"
2177
+ },
2178
+ "engines": {
2179
+ "node": ">=12.0.0"
2180
+ },
2181
+ "funding": {
2182
+ "url": "https://github.com/sponsors/SuperchupuDev"
2183
+ }
2184
+ },
2185
+ "node_modules/typescript": {
2186
+ "version": "5.8.3",
2187
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
2188
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
2189
+ "dev": true,
2190
+ "bin": {
2191
+ "tsc": "bin/tsc",
2192
+ "tsserver": "bin/tsserver"
2193
+ },
2194
+ "engines": {
2195
+ "node": ">=14.17"
2196
+ }
2197
+ },
2198
+ "node_modules/undici-types": {
2199
+ "version": "6.21.0",
2200
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
2201
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
2202
+ "dev": true
2203
+ },
2204
+ "node_modules/update-browserslist-db": {
2205
+ "version": "1.1.4",
2206
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
2207
+ "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
2208
+ "dev": true,
2209
+ "funding": [
2210
+ {
2211
+ "type": "opencollective",
2212
+ "url": "https://opencollective.com/browserslist"
2213
+ },
2214
+ {
2215
+ "type": "tidelift",
2216
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
2217
+ },
2218
+ {
2219
+ "type": "github",
2220
+ "url": "https://github.com/sponsors/ai"
2221
+ }
2222
+ ],
2223
+ "dependencies": {
2224
+ "escalade": "^3.2.0",
2225
+ "picocolors": "^1.1.1"
2226
+ },
2227
+ "bin": {
2228
+ "update-browserslist-db": "cli.js"
2229
+ },
2230
+ "peerDependencies": {
2231
+ "browserslist": ">= 4.21.0"
2232
+ }
2233
+ },
2234
+ "node_modules/vite": {
2235
+ "version": "6.4.1",
2236
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
2237
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
2238
+ "dev": true,
2239
+ "dependencies": {
2240
+ "esbuild": "^0.25.0",
2241
+ "fdir": "^6.4.4",
2242
+ "picomatch": "^4.0.2",
2243
+ "postcss": "^8.5.3",
2244
+ "rollup": "^4.34.9",
2245
+ "tinyglobby": "^0.2.13"
2246
+ },
2247
+ "bin": {
2248
+ "vite": "bin/vite.js"
2249
+ },
2250
+ "engines": {
2251
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
2252
+ },
2253
+ "funding": {
2254
+ "url": "https://github.com/vitejs/vite?sponsor=1"
2255
+ },
2256
+ "optionalDependencies": {
2257
+ "fsevents": "~2.3.3"
2258
+ },
2259
+ "peerDependencies": {
2260
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
2261
+ "jiti": ">=1.21.0",
2262
+ "less": "*",
2263
+ "lightningcss": "^1.21.0",
2264
+ "sass": "*",
2265
+ "sass-embedded": "*",
2266
+ "stylus": "*",
2267
+ "sugarss": "*",
2268
+ "terser": "^5.16.0",
2269
+ "tsx": "^4.8.1",
2270
+ "yaml": "^2.4.2"
2271
+ },
2272
+ "peerDependenciesMeta": {
2273
+ "@types/node": {
2274
+ "optional": true
2275
+ },
2276
+ "jiti": {
2277
+ "optional": true
2278
+ },
2279
+ "less": {
2280
+ "optional": true
2281
+ },
2282
+ "lightningcss": {
2283
+ "optional": true
2284
+ },
2285
+ "sass": {
2286
+ "optional": true
2287
+ },
2288
+ "sass-embedded": {
2289
+ "optional": true
2290
+ },
2291
+ "stylus": {
2292
+ "optional": true
2293
+ },
2294
+ "sugarss": {
2295
+ "optional": true
2296
+ },
2297
+ "terser": {
2298
+ "optional": true
2299
+ },
2300
+ "tsx": {
2301
+ "optional": true
2302
+ },
2303
+ "yaml": {
2304
+ "optional": true
2305
+ }
2306
+ }
2307
+ },
2308
+ "node_modules/web-streams-polyfill": {
2309
+ "version": "3.3.3",
2310
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
2311
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
2312
+ "engines": {
2313
+ "node": ">= 8"
2314
+ }
2315
+ },
2316
+ "node_modules/which": {
2317
+ "version": "2.0.2",
2318
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
2319
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
2320
+ "dependencies": {
2321
+ "isexe": "^2.0.0"
2322
+ },
2323
+ "bin": {
2324
+ "node-which": "bin/node-which"
2325
+ },
2326
+ "engines": {
2327
+ "node": ">= 8"
2328
+ }
2329
+ },
2330
+ "node_modules/wrap-ansi": {
2331
+ "version": "8.1.0",
2332
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
2333
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
2334
+ "dependencies": {
2335
+ "ansi-styles": "^6.1.0",
2336
+ "string-width": "^5.0.1",
2337
+ "strip-ansi": "^7.0.1"
2338
+ },
2339
+ "engines": {
2340
+ "node": ">=12"
2341
+ },
2342
+ "funding": {
2343
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
2344
+ }
2345
+ },
2346
+ "node_modules/wrap-ansi-cjs": {
2347
+ "name": "wrap-ansi",
2348
+ "version": "7.0.0",
2349
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
2350
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
2351
+ "dependencies": {
2352
+ "ansi-styles": "^4.0.0",
2353
+ "string-width": "^4.1.0",
2354
+ "strip-ansi": "^6.0.0"
2355
+ },
2356
+ "engines": {
2357
+ "node": ">=10"
2358
+ },
2359
+ "funding": {
2360
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
2361
+ }
2362
+ },
2363
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
2364
+ "version": "5.0.1",
2365
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
2366
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
2367
+ "engines": {
2368
+ "node": ">=8"
2369
+ }
2370
+ },
2371
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
2372
+ "version": "4.3.0",
2373
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
2374
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
2375
+ "dependencies": {
2376
+ "color-convert": "^2.0.1"
2377
+ },
2378
+ "engines": {
2379
+ "node": ">=8"
2380
+ },
2381
+ "funding": {
2382
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
2383
+ }
2384
+ },
2385
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
2386
+ "version": "8.0.0",
2387
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
2388
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
2389
+ },
2390
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
2391
+ "version": "4.2.3",
2392
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
2393
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
2394
+ "dependencies": {
2395
+ "emoji-regex": "^8.0.0",
2396
+ "is-fullwidth-code-point": "^3.0.0",
2397
+ "strip-ansi": "^6.0.1"
2398
+ },
2399
+ "engines": {
2400
+ "node": ">=8"
2401
+ }
2402
+ },
2403
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
2404
+ "version": "6.0.1",
2405
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
2406
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
2407
+ "dependencies": {
2408
+ "ansi-regex": "^5.0.1"
2409
+ },
2410
+ "engines": {
2411
+ "node": ">=8"
2412
+ }
2413
+ },
2414
+ "node_modules/ws": {
2415
+ "version": "8.18.3",
2416
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
2417
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
2418
+ "engines": {
2419
+ "node": ">=10.0.0"
2420
+ },
2421
+ "peerDependencies": {
2422
+ "bufferutil": "^4.0.1",
2423
+ "utf-8-validate": ">=5.0.2"
2424
+ },
2425
+ "peerDependenciesMeta": {
2426
+ "bufferutil": {
2427
+ "optional": true
2428
+ },
2429
+ "utf-8-validate": {
2430
+ "optional": true
2431
+ }
2432
+ }
2433
+ },
2434
+ "node_modules/yallist": {
2435
+ "version": "3.1.1",
2436
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
2437
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2438
+ "dev": true
2439
+ }
2440
+ }
2441
+ }
frontend/package.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "prism---lock-screen-classifier",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@google/genai": "^1.30.0",
13
+ "react": "^19.2.0",
14
+ "react-dom": "^19.2.0",
15
+ "react-router-dom": "^7.9.6"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.14.0",
19
+ "@vitejs/plugin-react": "^5.0.0",
20
+ "typescript": "~5.8.2",
21
+ "vite": "^6.2.0"
22
+ }
23
+ }
frontend/public/favicon.png ADDED

Git LFS Details

  • SHA256: 1a9f99048a3325c0a9c15bcb783495e9f373467d38181e448540515b65cf6019
  • Pointer size: 131 Bytes
  • Size of remote file: 469 kB
frontend/services/apiService.ts ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SingleAnalysisReport, BatchStreamResult } from "../types";
2
+
3
+ /**
4
+ * Uploads a single file to the Flask backend.
5
+ */
6
+ export const uploadSingle = async (file: File): Promise<string> => {
7
+ const formData = new FormData();
8
+ formData.append('file', file);
9
+
10
+ const response = await fetch('/upload_single', {
11
+ method: 'POST',
12
+ body: formData,
13
+ });
14
+
15
+ if (!response.ok) {
16
+ throw new Error(`Upload failed: ${response.statusText}`);
17
+ }
18
+
19
+ const data = await response.json();
20
+ return data.filename;
21
+ };
22
+
23
+ /**
24
+ * Triggers classification for a single image by filename.
25
+ * Expects the backend to return { result_table: "<html>..." }
26
+ */
27
+ export const classifySingle = async (filename: string): Promise<SingleAnalysisReport> => {
28
+ const response = await fetch('/classify_single', {
29
+ method: 'POST',
30
+ headers: {
31
+ 'Content-Type': 'application/json',
32
+ },
33
+ body: JSON.stringify({ filename }),
34
+ });
35
+
36
+ if (!response.ok) {
37
+ throw new Error(`Classification failed: ${response.statusText}`);
38
+ }
39
+
40
+ const data = await response.json();
41
+ return {
42
+ classification: data.classification,
43
+ detailed_results: data.detailed_results,
44
+ html: data.result_table // Optional fallback
45
+ };
46
+ };
47
+
48
+ /**
49
+ * Uploads multiple files for batch processing.
50
+ */
51
+ export const uploadMultiple = async (files: File[]): Promise<void> => {
52
+ const formData = new FormData();
53
+ files.forEach(file => {
54
+ formData.append('file', file);
55
+ });
56
+
57
+ const response = await fetch('/upload_multiple', {
58
+ method: 'POST',
59
+ body: formData,
60
+ });
61
+
62
+ if (!response.ok) {
63
+ throw new Error(`Batch upload failed: ${response.statusText}`);
64
+ }
65
+ // Assuming success means files are ready for classification
66
+ };
67
+
68
+ /**
69
+ * Triggers batch classification and returns the raw response for manual streaming.
70
+ */
71
+ export const classifyMultiple = async (): Promise<ReadableStream<Uint8Array>> => {
72
+ const response = await fetch('/classify_multiple', {
73
+ method: 'POST',
74
+ });
75
+
76
+ if (!response.ok || !response.body) {
77
+ throw new Error(`Batch classification failed: ${response.statusText}`);
78
+ }
79
+
80
+ return response.body;
81
+ };
82
+
83
+ /**
84
+ * Clears all uploaded files from the backend.
85
+ */
86
+ export const clearUploads = async () => {
87
+ const response = await fetch('/clear_uploads', {
88
+ method: 'POST',
89
+ });
90
+ return response.json();
91
+ };
92
+
93
+ export const getSamples = async () => {
94
+ const response = await fetch('/api/samples');
95
+ return response.json();
96
+ };
97
+
98
+ export const useSample = async (filename: string, destination: 'single' | 'multiple') => {
99
+ const response = await fetch('/api/use_sample', {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({ filename, destination })
103
+ });
104
+ return response.json();
105
+ };
106
+
107
+ /**
108
+ * Triggers batch classification and yields results as they stream in.
109
+ */
110
+ export async function* classifyMultipleStream(): AsyncGenerator<BatchStreamResult> {
111
+ const stream = await classifyMultiple();
112
+ const reader = stream.getReader();
113
+ const decoder = new TextDecoder();
114
+ let buffer = '';
115
+
116
+ try {
117
+ while (true) {
118
+ const { done, value } = await reader.read();
119
+ if (done) break;
120
+
121
+ buffer += decoder.decode(value, { stream: true });
122
+
123
+ // Process lines (assuming NDJSON or similar line-delimited JSON)
124
+ const lines = buffer.split('\n');
125
+ buffer = lines.pop() || ''; // Keep incomplete line in buffer
126
+
127
+ for (const line of lines) {
128
+ if (line.trim()) {
129
+ try {
130
+ const result = JSON.parse(line);
131
+ yield result as BatchStreamResult;
132
+ } catch (e) {
133
+ console.warn("Failed to parse stream chunk", e);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ } finally {
139
+ reader.releaseLock();
140
+ }
141
+ }
frontend/services/geminiService.ts ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { GoogleGenAI, Type, Schema } from "@google/genai";
2
+ import { AnalysisReport } from "../types";
3
+
4
+ // Initialize Gemini Client
5
+ // Note: API Key is injected via process.env.API_KEY
6
+ const ai = new GoogleGenAI({ apiKey: process.env.API_KEY });
7
+
8
+ const MODEL_NAME = "gemini-2.5-flash";
9
+
10
+ const analysisSchema: Schema = {
11
+ type: Type.OBJECT,
12
+ properties: {
13
+ isCompliant: { type: Type.BOOLEAN, description: "Whether the image is suitable for a public lock screen." },
14
+ overallScore: { type: Type.INTEGER, description: "A quality score from 0 to 100." },
15
+ checks: {
16
+ type: Type.ARRAY,
17
+ items: {
18
+ type: Type.OBJECT,
19
+ properties: {
20
+ name: { type: Type.STRING, description: "The name of the criterion being checked." },
21
+ passed: { type: Type.BOOLEAN, description: "Whether the check passed." },
22
+ reason: { type: Type.STRING, description: "Brief explanation of the result." }
23
+ },
24
+ required: ["name", "passed", "reason"]
25
+ }
26
+ }
27
+ },
28
+ required: ["isCompliant", "overallScore", "checks"]
29
+ };
30
+
31
+ /**
32
+ * Converts a File object to a Base64 string for the API.
33
+ */
34
+ const fileToGenerativePart = async (file: File): Promise<string> => {
35
+ return new Promise((resolve, reject) => {
36
+ const reader = new FileReader();
37
+ reader.onloadend = () => {
38
+ const base64String = reader.result as string;
39
+ // Remove data url prefix (e.g. "data:image/jpeg;base64,")
40
+ const base64Data = base64String.split(',')[1];
41
+ resolve(base64Data);
42
+ };
43
+ reader.onerror = reject;
44
+ reader.readAsDataURL(file);
45
+ });
46
+ };
47
+
48
+ export const analyzeLockScreen = async (file: File): Promise<AnalysisReport> => {
49
+ try {
50
+ const base64Data = await fileToGenerativePart(file);
51
+
52
+ const systemPrompt = `
53
+ You are Prism, an AI expert for Samsung Glance lock screen compliance.
54
+ Analyze the provided image against the following strict criteria:
55
+ 1. Image Quality: Must be high resolution, not blurry, no artifacts.
56
+ 2. Ribbon Detection: Ensure no promotional ribbons, watermarks, or text overlays covering the subject.
57
+ 3. Text Legibility: If there is text, is it legible? (Prefer no text for wallpapers).
58
+ 4. Safe Content: No offensive, violent, or adult content.
59
+ 5. Subject Centering: The main subject should be well-positioned for a mobile lock screen (portrait aspect).
60
+
61
+ Return the result as a structured JSON object.
62
+ `;
63
+
64
+ const response = await ai.models.generateContent({
65
+ model: MODEL_NAME,
66
+ contents: {
67
+ parts: [
68
+ { inlineData: { mimeType: file.type, data: base64Data } },
69
+ { text: systemPrompt }
70
+ ]
71
+ },
72
+ config: {
73
+ responseMimeType: "application/json",
74
+ responseSchema: analysisSchema,
75
+ temperature: 0.2, // Low temperature for consistent, objective analysis
76
+ }
77
+ });
78
+
79
+ if (response.text) {
80
+ return JSON.parse(response.text) as AnalysisReport;
81
+ } else {
82
+ throw new Error("No response text from Gemini.");
83
+ }
84
+
85
+ } catch (error) {
86
+ console.error("Gemini Analysis Failed:", error);
87
+ // Fallback mock error for UI stability if API fails completely
88
+ return {
89
+ isCompliant: false,
90
+ overallScore: 0,
91
+ checks: [
92
+ { name: "System Error", passed: false, reason: "Failed to connect to AI service." }
93
+ ]
94
+ };
95
+ }
96
+ };
frontend/tsconfig.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "experimentalDecorators": true,
5
+ "useDefineForClassFields": false,
6
+ "module": "ESNext",
7
+ "lib": [
8
+ "ES2022",
9
+ "DOM",
10
+ "DOM.Iterable"
11
+ ],
12
+ "skipLibCheck": true,
13
+ "types": [
14
+ "node"
15
+ ],
16
+ "moduleResolution": "bundler",
17
+ "isolatedModules": true,
18
+ "moduleDetection": "force",
19
+ "allowJs": true,
20
+ "jsx": "react-jsx",
21
+ "paths": {
22
+ "@/*": [
23
+ "./*"
24
+ ]
25
+ },
26
+ "allowImportingTsExtensions": true,
27
+ "noEmit": true
28
+ }
29
+ }
frontend/types.ts ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ export interface CheckResult {
3
+ name: string;
4
+ passed: boolean;
5
+ reason: string;
6
+ }
7
+
8
+ // Single analysis returns HTML string in result_table
9
+ export interface SingleAnalysisReport {
10
+ classification: string;
11
+ detailed_results: [string, string | number][];
12
+ html?: string; // Keeping for backward compatibility if needed, but optional
13
+ }
14
+
15
+ // Added AnalysisReport for geminiService
16
+ export interface AnalysisReport {
17
+ isCompliant: boolean;
18
+ overallScore: number;
19
+ checks: CheckResult[];
20
+ }
21
+
22
+ // Batch analysis streams JSON updates
23
+ export interface BatchStreamResult {
24
+ filename: string;
25
+ status: 'pass' | 'fail' | 'error';
26
+ score?: number;
27
+ details?: string;
28
+ labels?: string[];
29
+ error?: string;
30
+ }
31
+
32
+ export interface BatchItem {
33
+ id: string;
34
+ file: File;
35
+ previewUrl: string;
36
+ status: 'pending' | 'processing' | 'completed' | 'error';
37
+ result?: 'pass' | 'fail';
38
+ score?: number;
39
+ labels?: string[];
40
+ error?: string;
41
+ }
42
+
43
+ export type ViewState = 'home' | 'single' | 'batch';
frontend/vite.config.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+ import { defineConfig, loadEnv } from 'vite';
3
+ import react from '@vitejs/plugin-react';
4
+
5
+ export default defineConfig(({ mode }) => {
6
+ const env = loadEnv(mode, '.', '');
7
+ return {
8
+ build: {
9
+ outDir: '../static/react',
10
+ emptyOutDir: true,
11
+ },
12
+ base: '/static/react/',
13
+ server: {
14
+ port: 3000,
15
+ host: '0.0.0.0',
16
+ },
17
+ plugins: [react()],
18
+ define: {
19
+ 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
20
+ 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
21
+ },
22
+ resolve: {
23
+ alias: {
24
+ '@': path.resolve(__dirname, '.'),
25
+ }
26
+ }
27
+ };
28
+ });
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ easyocr
2
+ fuzzywuzzy
3
+ emoji
4
+ torch
5
+ torchvision
6
+ transformers
7
+
8
+ pillow
9
+ opencv-python-headless
10
+ tabulate
11
+ nltk
12
+ einops
13
+ timm
14
+ accelerate
15
+ flask
16
+ python-Levenshtein
17
+ numpy<2
static/certificates/excellence.png ADDED

Git LFS Details

  • SHA256: b84a2aafee602030eafeae8343d0c3cab0409ab750328e8365a728a079c56731
  • Pointer size: 131 Bytes
  • Size of remote file: 232 kB
static/certificates/participation.png ADDED

Git LFS Details

  • SHA256: b11ded2481197c22e72796ccdfbe8b567516b9c24c5c5ff9589640fb4ee077d9
  • Pointer size: 131 Bytes
  • Size of remote file: 207 kB
static/logo.png ADDED

Git LFS Details

  • SHA256: 0516322c781b87e75c32f1253982ea10608265617cf47e9334f1ccaba3ae3a07
  • Pointer size: 130 Bytes
  • Size of remote file: 24.3 kB
static/samples/1.png ADDED

Git LFS Details

  • SHA256: 58cb5ed4ada4f6a5d4991c9c4fb77311490184a0ea83926b37b7897e3421ab42
  • Pointer size: 132 Bytes
  • Size of remote file: 2.76 MB
static/samples/Angry husband trying to kill his wife indoors_ Concept of domestic violence.jpg ADDED

Git LFS Details

  • SHA256: 83afb93f634f3589555e9f3b8cbdbeee5bdd971b55ad49d43ab19be34a00537c
  • Pointer size: 130 Bytes
  • Size of remote file: 26.3 kB
static/samples/Custom Bloodied Scream Buck 120 Knife Prop (1).jpg ADDED

Git LFS Details

  • SHA256: 67d004ed79d9aa97a0a676b6d9d0dc8bf1c31cedeb8fab37a8b581e84d56bed3
  • Pointer size: 131 Bytes
  • Size of remote file: 128 kB
static/samples/NoTag2.jpg ADDED

Git LFS Details

  • SHA256: a44a5de577758b462e55a17df1c3f649b721d9598a4ce74342a352b32df42a7d
  • Pointer size: 131 Bytes
  • Size of remote file: 109 kB
static/samples/Picture4.png ADDED

Git LFS Details

  • SHA256: 28c98daedab262947b9ed3f1bed0cc1b64833b462ad1b635769fdbd768fec692
  • Pointer size: 131 Bytes
  • Size of remote file: 179 kB
static/samples/Screenshot_20241020_142718_One UI Home.jpg ADDED

Git LFS Details

  • SHA256: 6ac5bfc0fb37b327138c7d300e70492b686ef8c0b4e79d20042cf189210976fb
  • Pointer size: 131 Bytes
  • Size of remote file: 837 kB
static/samples/Screenshot_20241021-142831_One UI Home.jpg ADDED

Git LFS Details

  • SHA256: 708f5057e31348663bf5692f05a9473d1a7afba4d54fe8c6320b76d50cee0cdd
  • Pointer size: 131 Bytes
  • Size of remote file: 695 kB
static/samples/Screenshot_20241022-125250_One UI Home.jpg ADDED

Git LFS Details

  • SHA256: a5456f995857cdcaee700818753295d75a21172ba0b91e8b185ac913164b653e
  • Pointer size: 131 Bytes
  • Size of remote file: 972 kB
static/samples/Screenshot_20241022-133424_One UI Home.jpg ADDED

Git LFS Details

  • SHA256: f130f6d724b0c4e8a4ac94daf2d7af222933cea09e0bdbf2047208d7294b3a32
  • Pointer size: 132 Bytes
  • Size of remote file: 1.11 MB
static/samples/conf2.jpg ADDED

Git LFS Details

  • SHA256: e46d0333423e8d4a977f8a5f4f92ccaf2ce0d553d38cdc026f0e20cc6cea10a8
  • Pointer size: 131 Bytes
  • Size of remote file: 814 kB
static/samples/natraj.jpg ADDED

Git LFS Details

  • SHA256: 2943f0f0dfa6cfa48d16c9198809d894ccd86ccec855666c11f7163839bfa9ba
  • Pointer size: 131 Bytes
  • Size of remote file: 215 kB
static/samples/notEng.jpeg ADDED

Git LFS Details

  • SHA256: 67bdaa193af8a76bc411da7bd6ff85291f96a16730412de367cb723c925ed7cb
  • Pointer size: 131 Bytes
  • Size of remote file: 410 kB
static/samples/pixelcut-export (1).jpeg ADDED

Git LFS Details

  • SHA256: c9f5d996dd7016bec255f72485c301550f8ba2dc8cbeabf52dedce14ace5b8b9
  • Pointer size: 131 Bytes
  • Size of remote file: 237 kB
static/samples/pixelcut-export (2).png ADDED

Git LFS Details

  • SHA256: e5860d5eb958da632c1ae794f439f92f1d564712a8ebd1686abfd5a956823bd7
  • Pointer size: 132 Bytes
  • Size of remote file: 1.99 MB
static/samples/pixelcut-export (3).jpeg ADDED

Git LFS Details

  • SHA256: abb943dd5dabc2ce29eee05355cf96965d643f4f8f273a8d621f872ec89d2f70
  • Pointer size: 131 Bytes
  • Size of remote file: 233 kB