Commit
·
d790e98
0
Parent(s):
Initial deploy with LFS images and audio
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .agent/workflows/deploy_to_huggingface.md +73 -0
- .gitattributes +5 -0
- .gitignore +44 -0
- Dockerfile +38 -0
- app.py +299 -0
- backend/__init__.py +0 -0
- backend/config.py +109 -0
- backend/model_handler.py +157 -0
- backend/modules/__init__.py +0 -0
- backend/modules/content_checks.py +79 -0
- backend/modules/text_checks.py +125 -0
- backend/modules/visual_checks.py +99 -0
- backend/pipeline.py +151 -0
- backend/utils.py +131 -0
- deployment_guide.md +104 -0
- frontend/App.tsx +128 -0
- frontend/components/BackgroundAnimation.tsx +103 -0
- frontend/components/BatchAnalysis.tsx +389 -0
- frontend/components/Hero.tsx +113 -0
- frontend/components/Icons.tsx +64 -0
- frontend/components/SingleAnalysis.tsx +253 -0
- frontend/index.html +190 -0
- frontend/index.tsx +15 -0
- frontend/package-lock.json +2441 -0
- frontend/package.json +23 -0
- frontend/public/favicon.png +3 -0
- frontend/services/apiService.ts +141 -0
- frontend/services/geminiService.ts +96 -0
- frontend/tsconfig.json +29 -0
- frontend/types.ts +43 -0
- frontend/vite.config.ts +28 -0
- requirements.txt +17 -0
- static/certificates/excellence.png +3 -0
- static/certificates/participation.png +3 -0
- static/logo.png +3 -0
- static/samples/1.png +3 -0
- static/samples/Angry husband trying to kill his wife indoors_ Concept of domestic violence.jpg +3 -0
- static/samples/Custom Bloodied Scream Buck 120 Knife Prop (1).jpg +3 -0
- static/samples/NoTag2.jpg +3 -0
- static/samples/Picture4.png +3 -0
- static/samples/Screenshot_20241020_142718_One UI Home.jpg +3 -0
- static/samples/Screenshot_20241021-142831_One UI Home.jpg +3 -0
- static/samples/Screenshot_20241022-125250_One UI Home.jpg +3 -0
- static/samples/Screenshot_20241022-133424_One UI Home.jpg +3 -0
- static/samples/conf2.jpg +3 -0
- static/samples/natraj.jpg +3 -0
- static/samples/notEng.jpeg +3 -0
- static/samples/pixelcut-export (1).jpeg +3 -0
- static/samples/pixelcut-export (2).png +3 -0
- 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">© 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
|
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
|
static/certificates/participation.png
ADDED
|
Git LFS Details
|
static/logo.png
ADDED
|
Git LFS Details
|
static/samples/1.png
ADDED
|
Git LFS Details
|
static/samples/Angry husband trying to kill his wife indoors_ Concept of domestic violence.jpg
ADDED
|
Git LFS Details
|
static/samples/Custom Bloodied Scream Buck 120 Knife Prop (1).jpg
ADDED
|
Git LFS Details
|
static/samples/NoTag2.jpg
ADDED
|
Git LFS Details
|
static/samples/Picture4.png
ADDED
|
Git LFS Details
|
static/samples/Screenshot_20241020_142718_One UI Home.jpg
ADDED
|
Git LFS Details
|
static/samples/Screenshot_20241021-142831_One UI Home.jpg
ADDED
|
Git LFS Details
|
static/samples/Screenshot_20241022-125250_One UI Home.jpg
ADDED
|
Git LFS Details
|
static/samples/Screenshot_20241022-133424_One UI Home.jpg
ADDED
|
Git LFS Details
|
static/samples/conf2.jpg
ADDED
|
Git LFS Details
|
static/samples/natraj.jpg
ADDED
|
Git LFS Details
|
static/samples/notEng.jpeg
ADDED
|
Git LFS Details
|
static/samples/pixelcut-export (1).jpeg
ADDED
|
|
Git LFS Details
|
static/samples/pixelcut-export (2).png
ADDED
|
|
Git LFS Details
|
static/samples/pixelcut-export (3).jpeg
ADDED
|
|
Git LFS Details
|