diff --git a/.agent/workflows/deploy_to_huggingface.md b/.agent/workflows/deploy_to_huggingface.md new file mode 100644 index 0000000000000000000000000000000000000000..447fc4125462748b464a7416804ba940d98c5f6a --- /dev/null +++ b/.agent/workflows/deploy_to_huggingface.md @@ -0,0 +1,73 @@ +--- +description: Deploy to Hugging Face Spaces +--- + +# Deploying to Hugging Face Spaces + +This guide explains how to deploy the Samsung Prism Prototype to a Hugging Face Space and keep it synced with your local changes. + +## Prerequisites + +1. A Hugging Face account. +2. Git installed on your machine. + +## Step 1: Create a New Space + +1. Go to [huggingface.co/spaces](https://huggingface.co/spaces). +2. Click **"Create new Space"**. +3. **Name**: `samsung-prism-prototype` (or similar). +4. **License**: MIT (optional). +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). + * *Note*: Since we are using Flask, **Docker** is the most flexible option. Select **Docker** -> **Blank**. +6. Click **"Create Space"**. + +## Step 2: Prepare Your Project for Docker + +Ensure you have a `Dockerfile` in the root of your project. +(I will verify/create this for you in the next steps). + +## Step 3: Connect Local Repo to Hugging Face + +1. Initialize git if you haven't already: + ```bash + git init + ``` +2. Add the Hugging Face remote (replace `YOUR_USERNAME` and `SPACE_NAME`): + ```bash + git remote add space https://huggingface.co/spaces/YOUR_USERNAME/SPACE_NAME + ``` +3. Pull the initial files (like README.md) from the Space: + ```bash + git pull space main --allow-unrelated-histories + ``` + +## Step 4: Push Changes + +Whenever you make changes locally, run these commands to update the Space: + +```bash +# Add all changes +git add . + +# Commit changes +git commit -m "Update prototype" + +# Push to Hugging Face +git push space main +``` + +## Step 5: Handling Large Files (Models) + +**IMPORTANT**: Hugging Face Spaces have a hard limit on file sizes for Git (LFS). +Since your `Models/` directory is large, you should **NOT** push it to Git directly if the files are huge. +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. + +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. + +Create/Update `.gitignore`: +``` +Models/ +__pycache__/ +.venv/ +.env +``` diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..73c9fe27e7b6ae14b424fb511df3812a7cf66d71 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..4fe51a44e9fe60cd78078b1e7f3899d174bae231 --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +# Python +__pycache__/ +*.py[cod] +*$py.class +.venv/ +.env + +# Models (Large files) +Models/ + + +# Temporary/Testing +Testing ROIs for Ribbon/ + +# User Data +static/uploads/ +Reports/ + +# Build Artifacts (Built by Docker) +static/react/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..c2f0458af687296323f8c10bc570cf00e6997560 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +# Stage 1: Build React Frontend +FROM node:18-alpine as builder +WORKDIR /app +COPY frontend/ ./frontend/ +WORKDIR /app/frontend +RUN npm install +# Create the output directory structure expected by vite.config.ts +RUN mkdir -p ../static/react +RUN npm run build + +# Stage 2: Python Backend +FROM python:3.10-slim + +# Install system dependencies for OpenCV and others +RUN apt-get update && apt-get install -y \ + libgl1 \ + libglib2.0-0 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy requirements and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy backend code +COPY . . + +# Copy built frontend from builder stage +# Note: Vite build output was configured to ../static/react, so it's at /app/static/react in builder +COPY --from=builder /app/static/react ./static/react + +# Expose the port +# Expose the port (Hugging Face Spaces uses 7860) +EXPOSE 7860 + +# Command to run the application +CMD ["python", "app.py"] diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..1b967388edfee524a37dadb03ec7ef1b35b9ca8c --- /dev/null +++ b/app.py @@ -0,0 +1,299 @@ + +from flask import Flask, render_template, request, jsonify, send_file, Response, stream_with_context +from werkzeug.utils import secure_filename +import os +from pathlib import Path +import shutil +import io +import json +import logging +from backend.pipeline import classify + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +app = Flask(__name__) + +# Configuration +ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'} +UPLOAD_FOLDER_SINGLE = 'static/uploads/single' +UPLOAD_FOLDER_MULTIPLE = 'static/uploads/multiple' + +app.config['UPLOAD_FOLDER_SINGLE'] = UPLOAD_FOLDER_SINGLE +app.config['UPLOAD_FOLDER_MULTIPLE'] = UPLOAD_FOLDER_MULTIPLE +app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 +app.config['MAX_CONTENT_LENGTH_REPORT'] = 500 * 1024 * 1024 + +# Ensure upload directories exist +os.makedirs(UPLOAD_FOLDER_SINGLE, exist_ok=True) +os.makedirs(UPLOAD_FOLDER_MULTIPLE, exist_ok=True) + +def allowed_file(filename): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + +def clear_uploads(folder): + """Helper function to clear upload directories.""" + if os.path.exists(folder): + for filename in os.listdir(folder): + file_path = os.path.join(folder, filename) + try: + if os.path.isfile(file_path) or os.path.islink(file_path): + os.unlink(file_path) + elif os.path.isdir(file_path): + shutil.rmtree(file_path) + except Exception as e: + logger.error(f'Failed to delete {file_path}. Reason: {e}') + +@app.route('/', defaults={'path': ''}) +@app.route('/') +def index(path): + if path.startswith('static/'): + return send_file(path) + return send_file('static/react/index.html') + +@app.route('/upload_single', methods=['POST']) +def upload_single(): + if 'file' not in request.files: + return jsonify({'error': 'No file uploaded'}), 400 + file = request.files['file'] + if file.filename == '': + return jsonify({'error': 'No file selected'}), 400 + if file and allowed_file(file.filename): + filename = secure_filename(file.filename) + filepath = os.path.join(app.config['UPLOAD_FOLDER_SINGLE'], filename) + file.save(filepath) + return jsonify({'filename': filename}) + return jsonify({'error': 'Invalid file type'}), 400 + +@app.route('/classify_single', methods=['POST']) +def classify_single(): + data = request.get_json() + filename = data.get('filename') + if not filename: + return jsonify({'error': 'No filename provided'}), 400 + filepath = os.path.join(app.config['UPLOAD_FOLDER_SINGLE'], filename) + if not os.path.exists(filepath): + return jsonify({'error': 'File not found'}), 404 + + try: + classification_result, detailed_results, failure_labels = classify(filepath) + return jsonify({ + 'classification': classification_result, + 'detailed_results': detailed_results + }) + except Exception as e: + logger.error(f"Error in classify_single: {e}") + return jsonify({'error': str(e)}), 500 + +@app.route('/upload_multiple', methods=['POST']) +def upload_multiple(): + logger.info("=== UPLOAD_MULTIPLE CALLED ===") + if 'file' not in request.files: + logger.warning("No 'file' in request.files") + return jsonify({'error': 'No file uploaded'}), 400 + + files = request.files.getlist('file') + logger.info(f"Received {len(files)} files in request") + if not files: + return jsonify({'error': 'No files selected'}), 400 + + try: + # Ensure temp directory exists (DON'T wipe it) + temp_dir = os.path.join(app.config['UPLOAD_FOLDER_MULTIPLE'], 'temp') + os.makedirs(temp_dir, exist_ok=True) + + saved_count = 0 + filename_map = {} + + # Load existing map if present + map_path = os.path.join(temp_dir, 'filename_map.json') + if os.path.exists(map_path): + try: + with open(map_path, 'r') as f: + filename_map = json.load(f) + logger.info(f"Loaded existing filename map with {len(filename_map)} entries") + except Exception as e: + logger.error(f"Error loading existing filename map: {e}") + + for file in files: + if file and allowed_file(file.filename): + original_filename = file.filename + filename = secure_filename(file.filename) + filepath = os.path.join(temp_dir, filename) + file.save(filepath) + filename_map[filename] = original_filename + saved_count += 1 + logger.info(f"Saved: '{original_filename}' -> '{filename}'") + + # Save updated filename map + with open(map_path, 'w') as f: + json.dump(filename_map, f) + + # Count actual files in directory + actual_files = [f for f in os.listdir(temp_dir) if allowed_file(f)] + logger.info(f"Total files in temp directory after upload: {len(actual_files)}") + logger.info(f"Files: {actual_files}") + + return jsonify({ + 'message': f'Successfully uploaded {saved_count} files', + 'count': saved_count, + 'status': 'Ready' + }) + + except Exception as e: + logger.error(f"Error in upload_multiple: {e}", exc_info=True) + return jsonify({'error': str(e)}), 500 + +@app.route('/classify_multiple', methods=['POST']) +def classify_multiple(): + logger.info("=== CLASSIFY_MULTIPLE CALLED ===") + def generate(): + temp_dir = os.path.join(app.config['UPLOAD_FOLDER_MULTIPLE'], 'temp') + if not os.path.exists(temp_dir): + logger.warning("Temp directory does not exist") + yield json.dumps({'error': 'No files to classify'}) + '\n' + return + + # Load filename map + filename_map = {} + map_path = os.path.join(temp_dir, 'filename_map.json') + if os.path.exists(map_path): + try: + with open(map_path, 'r') as f: + filename_map = json.load(f) + logger.info(f"Loaded filename map: {filename_map}") + except Exception as e: + logger.error(f"Error loading filename map: {e}") + + files = [f for f in os.listdir(temp_dir) if allowed_file(f)] + logger.info(f"Processing {len(files)} files from temp directory") + logger.info(f"Files to process: {files}") + + for filename in files: + filepath = os.path.join(temp_dir, filename) + logger.info(f"Classifying: {filename}") + try: + classification_result, _, failure_labels = classify(filepath) + logger.info(f"Result for {filename}: {classification_result} with labels: {failure_labels}") + + # Move file + dest_dir = os.path.join(app.config['UPLOAD_FOLDER_MULTIPLE'], classification_result.lower()) + os.makedirs(dest_dir, exist_ok=True) + dest_path = os.path.join(dest_dir, filename) + shutil.move(filepath, dest_path) + + # Get original filename if available + original_filename = filename_map.get(filename, filename) + logger.info(f"Sending result for original filename: {original_filename}") + + result = { + 'filename': original_filename, + 'status': 'pass' if classification_result == 'Pass' else 'fail', + 'labels': failure_labels, + 'score': 0 + } + yield json.dumps(result) + '\n' + + except Exception as e: + logger.error(f"Error processing {filename}: {e}", exc_info=True) + # Use original filename for error reporting + original_filename = filename_map.get(filename, filename) + yield json.dumps({'filename': original_filename, 'status': 'error', 'error': str(e)}) + '\n' + + return Response(stream_with_context(generate()), mimetype='application/x-ndjson') + +@app.route('/clear_uploads', methods=['POST']) +def clear_uploads_route(): + logger.info("=== CLEAR_UPLOADS CALLED ===") + try: + clear_uploads(app.config['UPLOAD_FOLDER_SINGLE']) + clear_uploads(app.config['UPLOAD_FOLDER_MULTIPLE']) + return jsonify({'success': True}) + except Exception as e: + logger.error(f"Error in clear_uploads_route: {e}") + return jsonify({'error': str(e)}), 500 + +@app.route('/api/use_sample', methods=['POST']) +def use_sample(): + try: + data = request.get_json() + filename = data.get('filename') + destination = data.get('destination') # 'single' or 'multiple' + + if not filename or not destination: + return jsonify({'error': 'Missing filename or destination'}), 400 + + # Validate filename (security) + if not allowed_file(filename): + return jsonify({'error': 'Invalid filename'}), 400 + + # Source path + src_path = os.path.join('static', 'samples', filename) + if not os.path.exists(src_path): + return jsonify({'error': 'Sample not found'}), 404 + + # Destination path + if destination == 'single': + dest_folder = app.config['UPLOAD_FOLDER_SINGLE'] + elif destination == 'multiple': + dest_folder = os.path.join(app.config['UPLOAD_FOLDER_MULTIPLE'], 'temp') + os.makedirs(dest_folder, exist_ok=True) + else: + return jsonify({'error': 'Invalid destination'}), 400 + + dest_path = os.path.join(dest_folder, filename) + + # Copy file + shutil.copy2(src_path, dest_path) + + # For multiple, we need to update the filename map + if destination == 'multiple': + map_path = os.path.join(dest_folder, 'filename_map.json') + filename_map = {} + if os.path.exists(map_path): + try: + with open(map_path, 'r') as f: + filename_map = json.load(f) + except: + pass + + filename_map[filename] = filename # Map to itself for samples + + with open(map_path, 'w') as f: + json.dump(filename_map, f) + + return jsonify({'success': True, 'filename': filename}) + + except Exception as e: + logger.error(f"Error in use_sample: {e}") + return jsonify({'error': str(e)}), 500 + +@app.route('/api/samples', methods=['GET']) +def get_samples(): + try: + samples_dir = os.path.join('static', 'samples') + if not os.path.exists(samples_dir): + return jsonify([]) + + files = [f for f in os.listdir(samples_dir) if allowed_file(f)] + # Sort files for consistent order + files.sort() + + samples = [] + for i, filename in enumerate(files): + samples.append({ + 'id': i + 1, + 'url': f'/static/samples/{filename}', + 'filename': filename + }) + + return jsonify(samples) + except Exception as e: + logger.error(f"Error in get_samples: {e}") + return jsonify({'error': str(e)}), 500 + +if __name__ == '__main__': + logger.info("SERVER STARTING ON PORT 7860") + # Disable reloader to prevent loading models twice (saves memory) + app.run(debug=False, use_reloader=False, host='0.0.0.0', port=7860) diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000000000000000000000000000000000000..62261425b48e0d7932a8fa61bd4338158e2f1285 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,109 @@ + +# ROI Constants +BODY = (0.0, 0.0, 1.0, 1.0) +TAG = (0.05, 0.62, 1.0, 0.65) +DTAG = (0.05, 0.592, 1.0, 0.622) +TNC = (0.02, 0.98, 1.0, 1.0) +CTA = (0.68, 0.655, 0.87, 0.675) +GNC = (0.5, 0.652, 0.93, 0.77) + +# ROIs for Ribbon Detection +ROIS = [ + # Top section divided into 3 parts + (0.0, 0.612, 0.33, 0.626), # Top left + (0.33, 0.612, 0.66, 0.626), # Top middle + (0.66, 0.612, 1.0, 0.626), # Top right + + # Bottom section divided into 3 parts + (0.0, 0.678, 0.33, 0.686), # Bottom left + (0.33, 0.678, 0.66, 0.686), # Bottom middle + (0.66, 0.678, 1.0, 0.686), # Bottom right + + # Extreme Right section + (0.95, 0.63, 1, 0.678), + + # Middle Section (between Tag and Click) + (0.029, 0.648, 0.35, 0.658), # Middle left + (0.35, 0.648, 0.657, 0.658) # Middle right +] + +# Detection parameters for Ribbon +DETECTION_PARAMS = { + 'clahe_clip_limit': 2.0, + 'clahe_grid_size': (8, 8), + 'gaussian_kernel': (5, 5), + 'gaussian_sigma': 0, + 'canny_low': 20, + 'canny_high': 80, + 'hough_threshold': 15, + 'min_line_length': 10, + 'max_line_gap': 5, + 'edge_pixel_threshold': 0.01 +} + +# Prompts +PTAG = "Extract all the text from the image accurately." +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." +PGNC = "Is there a HAND POINTER/EMOJI or a LARGE ARROW or ARROW POINTER? Answer only 'yes' or 'no'." + +# Lists for Content Checks +RISKY_KEYWORDS = [ + # General gambling terms + "casino", "poker", "jackpot", "blackjack", + "sports betting", "online casino", "slot machine", "pokies", + + # Gambling website and app names (Global and India-promoted) + "stake", "betano", "bet365", "888casino", "ladbrokes", "betfair", + "unibet", "skybet", "coral", "betway", "sportingbet", "betvictor", "partycasino", "casinocom", "jackpot city", + "playtech", "meccabingo", "fanDuel", "betmobile", "10bet", "10cric", + "pokerstars" "fulltiltpoker", "wsop", + + # Gambling websites and apps promoted or popular in India + "dream11", "dreamll", "my11circle", "cricbuzz", "fantasy cricket", "sportz exchange", "fun88", + "funbb", "funbeecom", "funbee", "rummycircle", "pokertiger", "adda52", "khelplay", + "paytm first games", "fanmojo", "betking", "1xbet", "parimatch", "rajapoker", + + # High-risk trading and investment terms + "win cash", "high risk trading", "win lottery", + "high risk investment", "investment scheme", + "get rich quick", "trading signals", "financial markets", "day trading", + "options trading", "forex signals" +] + +ILLEGAL_ACTIVITIES = [ + "hack", "hacking", "cheating", "cheat", "drugs", "drug", "steal", "stealing", + "phishing", "phish", "piracy", "pirate", "fraud", "smuggling", "smuggle", + "counterfeiting", "blackmailing", "blackmail", "extortion", "scamming", "scam", + "identity theft", "illegal trading", "money laundering", "poaching", "poach", + "trafficking", "illegal arms", "explosives", "bomb", "bombing", "fake documents" +] + +ILLEGAL_PHRASES = [ + "how to", "learn", "steps to", "guide to", "ways to", + "tutorial on", "methods for", "process of", + "tricks for", "shortcuts to", "make" +] + +COMPETITOR_BRANDS = [ + "motorola", "oppo", "vivo", "htc", "sony", "nokia", "honor", "huawei", "asus", "lg", + "oneplus", "apple", "micromax", "lenovo", "gionee", "infocus", "lava", "panasonic","intex", + "blackberry", "xiaomi", "philips", "godrej", "whirlpool", "blue star", "voltas", + "hitachi", "realme", "poco", "iqoo", "toshiba", "skyworth", "redmi", "nokia", "lava" +] + +APPROPRIATE_LABELS = [ + "Inappropriate Content: Violence, Blood, political promotion, drugs, alcohol, cigarettes, smoking, cruelty, nudity, illegal activities", + "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" +] + +RELIGIOUS_LABELS = [ + "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", + "Hindu Deity / OM or AUM or Swastik symbol", + "Jesus Christ / Christianity Cross" +] + +# Image Quality Thresholds +MIN_WIDTH = 720 +MIN_HEIGHT = 1600 +MIN_PIXEL_COUNT = 1000000 +PIXEL_VARIANCE_THRESHOLD = 50 diff --git a/backend/model_handler.py b/backend/model_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..4249d77c0c9f55638713bba16ecd05f94bbf9e01 --- /dev/null +++ b/backend/model_handler.py @@ -0,0 +1,157 @@ +import os +import torch +import easyocr +import numpy as np +import gc +from transformers import AutoTokenizer, AutoModel, AutoProcessor, AutoModelForZeroShotImageClassification +import torch.nn.functional as F +from backend.utils import build_transform + +class ModelHandler: + def __init__(self): + try: + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + print(f"Using device: {self.device}", flush=True) + self.transform = build_transform() + self.load_models() + except Exception as e: + print(f"CRITICAL ERROR in ModelHandler.__init__: {e}", flush=True) + import traceback + traceback.print_exc() + + def load_models(self): + # MODEL 1: InternVL + try: + # Check if local path exists, otherwise use HF Hub ID + local_path = os.path.join("Models", "InternVL2_5-1B-MPO") + if os.path.exists(local_path): + internvl_model_path = local_path + print(f"Loading InternVL from local path: {internvl_model_path}", flush=True) + else: + internvl_model_path = "OpenGVLab/InternVL2_5-1B-MPO" # HF Hub ID + print(f"Local model not found. Downloading InternVL from HF Hub: {internvl_model_path}", flush=True) + + self.model_int = AutoModel.from_pretrained( + internvl_model_path, + torch_dtype=torch.bfloat16, + low_cpu_mem_usage=True, + trust_remote_code=True + ).eval() + + for module in self.model_int.modules(): + if isinstance(module, torch.nn.Dropout): + module.p = 0 + + self.tokenizer_int = AutoTokenizer.from_pretrained(internvl_model_path, trust_remote_code=True) + print("\nInternVL model and tokenizer loaded successfully.", flush=True) + except Exception as e: + print(f"\nError loading InternVL model or tokenizer: {e}", flush=True) + import traceback + traceback.print_exc() + self.model_int = None + self.tokenizer_int = None + + # MODEL 2: EasyOCR + try: + # EasyOCR automatically handles downloading if not present + self.reader = easyocr.Reader(['en', 'hi'], gpu=False) + print("\nEasyOCR reader initialized successfully.") + except Exception as e: + print(f"\nError initializing EasyOCR reader: {e}") + self.reader = None + + # MODEL 3: CLIP + try: + local_path = os.path.join("Models", "clip-vit-base-patch32") + if os.path.exists(local_path): + clip_model_path = local_path + print(f"Loading CLIP from local path: {clip_model_path}") + else: + clip_model_path = "openai/clip-vit-base-patch32" # HF Hub ID + print(f"Local model not found. Downloading CLIP from HF Hub: {clip_model_path}") + + self.processor_clip = AutoProcessor.from_pretrained(clip_model_path) + self.model_clip = AutoModelForZeroShotImageClassification.from_pretrained(clip_model_path).to(self.device) + print("\nCLIP model and processor loaded successfully.") + except Exception as e: + print(f"\nError loading CLIP model or processor: {e}") + self.model_clip = None + self.processor_clip = None + + def easyocr_ocr(self, image): + if not self.reader: + return "" + image_np = np.array(image) + results = self.reader.readtext(image_np, detail=1) + + del image_np + gc.collect() + + if not results: + return "" + + sorted_results = sorted(results, key=lambda x: (x[0][0][1], x[0][0][0])) + ordered_text = " ".join([res[1] for res in sorted_results]).strip() + return ordered_text + + def intern(self, image, prompt, max_tokens): + if not self.model_int or not self.tokenizer_int: + return "" + + pixel_values = self.transform(image).unsqueeze(0).to(self.device).to(torch.bfloat16) + with torch.no_grad(): + response, _ = self.model_int.chat( + self.tokenizer_int, + pixel_values, + prompt, + generation_config={ + "max_new_tokens": max_tokens, + "do_sample": False, + "num_beams": 1, + "temperature": 1.0, + "top_p": 1.0, + "repetition_penalty": 1.0, + "length_penalty": 1.0, + "pad_token_id": self.tokenizer_int.pad_token_id + }, + history=None, + return_history=True + ) + + del pixel_values + gc.collect() + return response + + def clip(self, image, labels): + if not self.model_clip or not self.processor_clip: + return None + + processed = self.processor_clip( + text=labels, + images=image, + padding=True, + return_tensors="pt" + ).to(self.device) + + del image, labels + gc.collect() + return processed + + def get_clip_probs(self, image, labels): + inputs = self.clip(image, labels) + if inputs is None: + return None + + with torch.no_grad(): + outputs = self.model_clip(**inputs) + + logits_per_image = outputs.logits_per_image + probs = F.softmax(logits_per_image, dim=1) + + del inputs, outputs, logits_per_image + gc.collect() + + return probs + +# Create a global instance to be used by modules +model_handler = ModelHandler() diff --git a/backend/modules/__init__.py b/backend/modules/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/modules/content_checks.py b/backend/modules/content_checks.py new file mode 100644 index 0000000000000000000000000000000000000000..5e6191f8abbe8f18b9bf7ad8c995ce3f80c96b74 --- /dev/null +++ b/backend/modules/content_checks.py @@ -0,0 +1,79 @@ +import re +import torch +from PIL import Image +from backend import config +from backend.utils import find_similar_substring, destroy_text_roi +from backend.model_handler import model_handler + +def is_risky(body_text): + body_text = re.sub(r'[^a-zA-Z0-9\u0966-\u096F\s]', '', body_text) + for keyword in config.RISKY_KEYWORDS: + if find_similar_substring(body_text, keyword): + return True + return False + +def is_prom_illegal_activity(body_text): + for phrase in config.ILLEGAL_PHRASES: + for activity in config.ILLEGAL_ACTIVITIES: + pattern = rf"{re.escape(phrase)}.*?{re.escape(activity)}" + if re.search(pattern, body_text): + return True + return False + +def is_competitor(body_text): + for brand in config.COMPETITOR_BRANDS: + if re.search(r'\b' + re.escape(brand) + r'\b', body_text): + return True + return False + +def body(image_path): + results = {} + image = Image.open(image_path).convert('RGB') + bd = model_handler.intern(image, config.PTAG, 500).lower() + ocr_substitutions = {'0': 'o', '1': 'l', '!': 'l', '@': 'a', '5': 's', '8': 'b'} + + for char, substitute in ocr_substitutions.items(): + bd = bd.replace(char, substitute) + bd = ' '.join(bd.split()) + + results["High Risk Content"] = 1 if is_risky(bd) else 0 + results["Illegal Content"] = 1 if is_prom_illegal_activity(bd) else 0 + results["Competitor References"] = 1 if is_competitor(bd) else 0 + + return results + +def offensive(image): + image = destroy_text_roi(image, *config.TAG) + + probs = model_handler.get_clip_probs(image, config.APPROPRIATE_LABELS) + if probs is None: + return False + + inappropriate_prob = probs[0][0].item() + appropriate_prob = probs[0][1].item() + + if inappropriate_prob > appropriate_prob: + return True + return False + +def religious(image): + probs = model_handler.get_clip_probs(image, config.RELIGIOUS_LABELS) + if probs is None: + return False, None + + highest_score_index = torch.argmax(probs, dim=1).item() + + if highest_score_index != 0: + return True, config.RELIGIOUS_LABELS[highest_score_index] + return False, None + +def theme(image_path): + results = {} + image = Image.open(image_path).convert('RGB') + + results["Inappropriate Content"] = 1 if offensive(image) else 0 + + is_religious, religious_label = religious(image) + results["Religious Content"] = f"1 [{religious_label}]" if is_religious else "0" + + return results diff --git a/backend/modules/text_checks.py b/backend/modules/text_checks.py new file mode 100644 index 0000000000000000000000000000000000000000..f3662445df8803282af0793126ac8cf91aba8b1e --- /dev/null +++ b/backend/modules/text_checks.py @@ -0,0 +1,125 @@ +import re +import emoji +from PIL import Image +from backend import config +from backend.utils import get_roi, clean_text, are_strings_similar, blur_image, is_blank, is_english, is_valid_english, destroy_text_roi +from backend.model_handler import model_handler + +def is_unreadable_tagline(htag, tag): + clean_htag = clean_text(htag) + clean_tag = clean_text(tag) + return not are_strings_similar(clean_htag, clean_tag) + +def is_hyperlink_tagline(tag): + substrings = ['www', '.com', 'http'] + return any(sub in tag for sub in substrings) + +def is_price_tagline(tag): + exclude_keywords = ["crore", "thousand", "million", "billion", "trillion"] + 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))' + price_pattern = r'(₹\s?\d+)|(\brs\.?\s?\d+)|(\$\s?\d+)|(र\d+)' + + if any(keyword in tag for keyword in exclude_keywords): + return False + if re.search(exclude_pattern, tag): + return False + return bool(re.search(price_pattern, tag)) + +def is_multiple_emoji(emoji_text): + words = emoji_text.split() + last_word = words[-1] + return last_word not in ['0', '1'] + +def is_incomplete_tagline(tag, is_eng): + tag = emoji.replace_emoji(tag, '') + tag = tag.strip() + if tag.endswith(('...', '..')): + return True + if not is_eng and tag.endswith(('.')): + return True + return False + +def tagline(image_path): + results = { + "Empty/Illegible/Black Tagline": 0, + "Multiple Taglines": 0, + "Incomplete Tagline": 0, + "Hyperlink": 0, + "Price Tag": 0, + "Excessive Emojis": 0 + } + + image = get_roi(image_path, *config.TAG) + himage = blur_image(image, 0.3) + easytag = model_handler.easyocr_ocr(image).lower().strip() + unr = model_handler.easyocr_ocr(himage).lower().strip() + + if is_blank(easytag) or is_blank(unr): + results["Empty/Illegible/Black Tagline"] = 1 + return results + + is_eng = is_english(easytag) + if not is_eng: + results["Empty/Illegible/Black Tagline"] = 0 + tag = easytag + else: + Tag = model_handler.intern(image, config.PTAG, 25).strip() + tag = Tag.lower() + + htag = model_handler.intern(himage, config.PTAG, 25).lower().strip() + if is_unreadable_tagline(htag, tag): + results["Empty/Illegible/Black Tagline"] = 1 + + results["Incomplete Tagline"] = 1 if is_incomplete_tagline(tag, is_eng) else 0 + results["Hyperlink"] = 1 if is_hyperlink_tagline(tag) else 0 + results["Price Tag"] = 1 if is_price_tagline(tag) else 0 + + imagedt = get_roi(image_path, *config.DTAG) + dtag = model_handler.easyocr_ocr(imagedt).strip() + results["Multiple Taglines"] = 0 if is_blank(dtag) else 1 + + emoji_resp = model_handler.intern(image, config.PEMO, 100) + results["Excessive Emojis"] = 1 if is_multiple_emoji(emoji_resp) else 0 + + return results + +def cta(image_path): + image = get_roi(image_path, *config.CTA) + cta_text = model_handler.intern(image, config.PTAG, 5).strip() + veng = is_valid_english(cta_text) + eng = is_english(cta_text) + + if '.' in cta_text or '..' in cta_text or '...' in cta_text: + return {"Bad CTA": 1} + + if any(emoji.is_emoji(c) for c in cta_text): + return {"Bad CTA": 1} + + clean_cta_text = clean_text(cta_text) + # print(len(clean_cta_text)) # Removed print + + if eng and len(clean_cta_text) <= 2: + return {"Bad CTA": 1} + + if len(clean_cta_text) > 15: + return {"Bad CTA": 1} + + return {"Bad CTA": 0} + +def tnc(image_path): + image = get_roi(image_path, *config.TNC) + tnc_text = model_handler.easyocr_ocr(image) + clean_tnc = clean_text(tnc_text) + + return {"Terms & Conditions": 0 if is_blank(clean_tnc) else 1} + +def tooMuchText(image_path): + DRIB = (0.04, 0.625, 1.0, 0.677) + DUP = (0, 0, 1.0, 0.25) + DBEL = (0, 0.85, 1.0, 1) + image = Image.open(image_path).convert('RGB') + image = destroy_text_roi(image, *DRIB) + image = destroy_text_roi(image, *DUP) + image = destroy_text_roi(image, *DBEL) + bd = model_handler.easyocr_ocr(image).lower().strip() + return {"Too Much Text": 1 if len(bd) > 55 else 0} diff --git a/backend/modules/visual_checks.py b/backend/modules/visual_checks.py new file mode 100644 index 0000000000000000000000000000000000000000..aa5f5a44388ac7abf2f11de0dcceed5a218379ed --- /dev/null +++ b/backend/modules/visual_checks.py @@ -0,0 +1,99 @@ +import cv2 +import numpy as np +from PIL import Image +from backend import config +from backend.utils import get_roi +from backend.model_handler import model_handler + +def detect_straight_lines(roi_img): + """Enhanced edge detection focusing on straight lines.""" + gray = cv2.cvtColor(roi_img, cv2.COLOR_BGR2GRAY) + clahe = cv2.createCLAHE( + clipLimit=config.DETECTION_PARAMS['clahe_clip_limit'], + tileGridSize=config.DETECTION_PARAMS['clahe_grid_size'] + ) + enhanced = clahe.apply(gray) + blurred = cv2.GaussianBlur( + enhanced, + config.DETECTION_PARAMS['gaussian_kernel'], + config.DETECTION_PARAMS['gaussian_sigma'] + ) + edges = cv2.Canny( + blurred, + config.DETECTION_PARAMS['canny_low'], + config.DETECTION_PARAMS['canny_high'] + ) + line_mask = np.zeros_like(edges) + lines = cv2.HoughLinesP( + edges, + rho=1, + theta=np.pi/180, + threshold=config.DETECTION_PARAMS['hough_threshold'], + minLineLength=config.DETECTION_PARAMS['min_line_length'], + maxLineGap=config.DETECTION_PARAMS['max_line_gap'] + ) + if lines is not None: + for line in lines: + x1, y1, x2, y2 = line[0] + cv2.line(line_mask, (x1, y1), (x2, y2), 255, 2) + return line_mask + +def simple_edge_detection(roi_img): + """Simple edge detection.""" + gray = cv2.cvtColor(roi_img, cv2.COLOR_BGR2GRAY) + return cv2.Canny(gray, 50, 150) + +def ribbon(image_path): + """Detect the presence of a ribbon in an image.""" + image = cv2.imread(image_path) + if image is None: + raise ValueError(f"Could not read image: {image_path}") + + h, w = image.shape[:2] + edge_present = [] + + for i, roi in enumerate(config.ROIS): + x1, y1, x2, y2 = [int(coord * (w if i % 2 == 0 else h)) for i, coord in enumerate(roi)] + roi_img = image[y1:y2, x1:x2] + + if i < 6: # Straight line detection for ROIs 0-5 + edges = detect_straight_lines(roi_img) + edge_present.append(np.sum(edges) > edges.size * config.DETECTION_PARAMS['edge_pixel_threshold']) + else: # Original method for ROIs 6-8 + edges = simple_edge_detection(roi_img) + edge_present.append(np.any(edges)) + + result = all(edge_present[:6]) and not edge_present[6] and not edge_present[7] and not edge_present[8] + return {"No Ribbon": 0 if result else 1} + +def image_quality(image_path): + """ + Check if an image is low resolution or poor quality. + """ + try: + image = Image.open(image_path) + width, height = image.size + pixel_count = width * height + + if width < config.MIN_WIDTH or height < config.MIN_HEIGHT or pixel_count < config.MIN_PIXEL_COUNT: + return {"Bad Image Quality": 1} + + grayscale_image = image.convert("L") + pixel_array = np.array(grayscale_image) + variance = np.var(pixel_array) + + if variance < config.PIXEL_VARIANCE_THRESHOLD: + return {"Bad Image Quality": 1} + + return {"Bad Image Quality": 0} + + except Exception as e: + print(f"Error processing image: {e}") + return {"Bad Image Quality": 1} + +def gnc(image_path): + """Check for gestures/coach marks and display the image.""" + image = get_roi(image_path, *config.GNC) + gnc_text = model_handler.intern(image, config.PGNC, 900).lower() + + return {"Visual Gesture or Icon": 1 if 'yes' in gnc_text else 0} diff --git a/backend/pipeline.py b/backend/pipeline.py new file mode 100644 index 0000000000000000000000000000000000000000..a3db84213e2cfcc004463098f779b641341e183c --- /dev/null +++ b/backend/pipeline.py @@ -0,0 +1,151 @@ +from backend.modules import visual_checks, text_checks, content_checks +import logging +import random +import time + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# ========================================== +# REAL CLASSIFICATION LOGIC +# ========================================== +def classify_real(image_path): + """Perform complete classification with detailed results using AI models.""" + # Components to check + components = [ + visual_checks.image_quality, + visual_checks.ribbon, + text_checks.tagline, + text_checks.tooMuchText, + content_checks.theme, + content_checks.body, + text_checks.cta, + text_checks.tnc, + visual_checks.gnc + ] + + # Collect all results + all_results = {} + for component in components: + try: + results = component(image_path) + all_results.update(results) + except Exception as e: + logger.error(f"Error in component {component.__name__}: {e}") + pass + + # Calculate final classification + final_classification = 0 + for result in all_results.values(): + if isinstance(result, int): + if result == 1: + final_classification = 1 + break + elif isinstance(result, str): + if result.startswith('1'): + final_classification = 1 + break + + # Determine Pass or Fail + classification_result = "Fail" if final_classification == 1 else "Pass" + + # Prepare the table data + table_data = [] + labels = [ + "Bad Image Quality", "No Ribbon", "Empty/Illegible/Black Tagline", "Multiple Taglines", + "Incomplete Tagline", "Hyperlink", "Price Tag", "Excessive Emojis", "Too Much Text", + "Inappropriate Content", "Religious Content", "High Risk Content", + "Illegal Content", "Competitor References", "Bad CTA", "Terms & Conditions", + "Visual Gesture or Icon" + ] + + # Collect labels responsible for failure + failure_labels = [] + for label in labels: + result = all_results.get(label, 0) + + is_fail = False + if isinstance(result, int) and result == 1: + is_fail = True + elif isinstance(result, str) and result.startswith('1'): + is_fail = True + + if is_fail: + failure_labels.append(label) + + table_data.append([label, result]) + + # Return the final classification, result table data, and failure labels (if any) + return classification_result, table_data, failure_labels + +# ========================================== +# DUMMY CLASSIFICATION FOR TESTING +# ========================================== +def classify_dummy(image_path): + """ + A dummy classification function that returns random results. + Useful for testing the frontend without running expensive models. + """ + # Simulate processing time + time.sleep(1) + + all_results = { + "Bad Image Quality": 1, + "No Ribbon": random.choice([0, 1]), + "Empty/Illegible/Black Tagline": 1, + "Multiple Taglines": 1, + "Incomplete Tagline": 1, + "Hyperlink": 1, + "Price Tag": 1, + "Excessive Emojis": 1, + "Too Much Text": 1, + "Inappropriate Content": 1, + "Religious Content": 1, + "High Risk Content": 1, + "Illegal Content": 1, + "Competitor References": 0, + "Bad CTA": 0, + "Terms & Conditions": 0, + "Visual Gesture or Icon": 1 + } + + # Determine Pass/Fail based on results + final_classification = 0 + for result in all_results.values(): + if isinstance(result, int) and result == 1: + final_classification = 1 + break + elif isinstance(result, str) and result.startswith('1'): + final_classification = 1 + break + + classification_result = "Fail" if final_classification == 1 else "Pass" + + # Collect failure labels and prepare table data + labels = list(all_results.keys()) + failure_labels = [] + table_data = [] + + for label in labels: + result = all_results[label] + is_fail = False + if isinstance(result, int) and result == 1: + is_fail = True + elif isinstance(result, str) and result.startswith('1'): + is_fail = True + + if is_fail: + failure_labels.append(label) + + table_data.append([label, result]) + + return classification_result, table_data, failure_labels + +# ========================================== +# TOGGLE CLASSIFIER HERE +# ========================================== +# Uncomment the one you want to use + +# classify = classify_dummy +classify = classify_real \ No newline at end of file diff --git a/backend/utils.py b/backend/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..ab9f1e1a5efd905208ad656607de1417dd20fc47 --- /dev/null +++ b/backend/utils.py @@ -0,0 +1,131 @@ +import re +import numpy as np +import cv2 +from PIL import Image +import random +import torch +import torchvision.transforms as T +from torchvision.transforms.functional import InterpolationMode +from difflib import SequenceMatcher +from nltk.metrics.distance import edit_distance +import nltk + +# Ensure NLTK data is downloaded +try: + nltk.data.find('corpora/words.zip') +except LookupError: + nltk.download('words') +try: + nltk.data.find('tokenizers/punkt') +except LookupError: + nltk.download('punkt') + +from nltk.corpus import words + +def set_seed(seed=42): + random.seed(seed) + np.random.seed(seed) + torch.manual_seed(seed) + # torch.cuda.manual_seed_all(seed) # Uncomment if using GPU + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + +def build_transform(input_size=448): + mean = (0.485, 0.456, 0.406) + std = (0.229, 0.224, 0.225) + return T.Compose([ + T.Lambda(lambda img: img.convert('RGB')), + T.Resize((input_size, input_size), interpolation=InterpolationMode.BICUBIC), + T.ToTensor(), + T.Normalize(mean=mean, std=std) + ]) + +def get_roi(image_path_or_obj, *roi): + """ + Extracts ROI from an image path or PIL Image object. + """ + if isinstance(image_path_or_obj, str): + image = Image.open(image_path_or_obj).convert('RGB') + else: + image = image_path_or_obj.convert('RGB') + + width, height = image.size + + roi_x_start = int(width * roi[0]) + roi_y_start = int(height * roi[1]) + roi_x_end = int(width * roi[2]) + roi_y_end = int(height * roi[3]) + + cropped_image = image.crop((roi_x_start, roi_y_start, roi_x_end, roi_y_end)) + return cropped_image + +def clean_text(text): + return re.sub(r'[^a-zA-Z0-9]', '', text).strip().lower() + +def are_strings_similar(str1, str2, max_distance=3, max_length_diff=2): + if str1 == str2: + return True + if abs(len(str1) - len(str2)) > max_length_diff: + return False + edit_distance_value = edit_distance(str1, str2) + return edit_distance_value <= max_distance + +def blur_image(image, strength): + image_np = np.array(image) + blur_strength = int(strength * 50) + blur_strength = max(1, blur_strength | 1) + blurred_image = cv2.GaussianBlur(image_np, (blur_strength, blur_strength), 0) + blurred_pil_image = Image.fromarray(blurred_image) + return blurred_pil_image + +def is_blank(text, limit=15): + return len(text) < limit + +def string_similarity(a, b): + return SequenceMatcher(None, a.lower(), b.lower()).ratio() + +def find_similar_substring(text, keyword, threshold=0.9): + text = text.lower() + keyword = keyword.lower() + + if keyword in text: + return True + + keyword_length = len(keyword.split()) + words_list = text.split() + + for i in range(len(words_list) - keyword_length + 1): + phrase = ' '.join(words_list[i:i + keyword_length]) + similarity = string_similarity(phrase, keyword) + if similarity >= threshold: + return True + + return False + +def destroy_text_roi(image, *roi_params): + image_np = np.array(image) + + h, w, _ = image_np.shape + x1 = int(roi_params[0] * w) + y1 = int(roi_params[1] * h) + x2 = int(roi_params[2] * w) + y2 = int(roi_params[3] * h) + + roi = image_np[y1:y2, x1:x2] + + blurred_roi = cv2.GaussianBlur(roi, (75, 75), 0) + noise = np.random.randint(0, 50, (blurred_roi.shape[0], blurred_roi.shape[1], 3), dtype=np.uint8) + noisy_blurred_roi = cv2.add(blurred_roi, noise) + image_np[y1:y2, x1:x2] = noisy_blurred_roi + return Image.fromarray(image_np) + +def is_english(text): + allowed_pattern = re.compile( + r'^[a-zA-Z०-९\u0930\s\.,!?\-;:"\'()]*$' + ) + return bool(allowed_pattern.match(text)) + +def is_valid_english(text): + english_words = set(words.words()) + cleaned_words = ''.join(c.lower() if c.isalnum() else ' ' for c in text).split() + return all(word.lower() in english_words for word in cleaned_words) diff --git a/deployment_guide.md b/deployment_guide.md new file mode 100644 index 0000000000000000000000000000000000000000..9a532c32207ecc6b07c16dbd6a035ef4fbe5f75a --- /dev/null +++ b/deployment_guide.md @@ -0,0 +1,104 @@ +# Deployment Guide: Hugging Face Spaces + +This guide will help you deploy your **Prism** application to Hugging Face Spaces using Docker. + +## Prerequisites + +1. A [Hugging Face account](https://huggingface.co/join). +2. Git installed on your computer. + +## Step 1: Create a New Space + +1. Go to [huggingface.co/new-space](https://huggingface.co/new-space). +2. **Space Name**: Enter a name (e.g., `prism-classifier`). +3. **License**: Choose a license (e.g., MIT) or leave blank. +4. **SDK**: Select **Docker**. +5. **Space Hardware**: Select **CPU Basic (Free)** (or upgrade if you need more power for AI models). +6. **Visibility**: Public or Private. +7. Click **Create Space**. + +## Step 2: Setup Git for Deployment + +You need to link your local project to the Hugging Face Space. + +1. Open your terminal in the project root (`d:\My Stuff\Coding Projects\Prism`). +2. Initialize Git (if not already done): + ```bash + git init + ``` +3. Add the Hugging Face remote (replace `YOUR_USERNAME` and `SPACE_NAME`): + ```bash + git remote add space https://huggingface.co/spaces/YOUR_USERNAME/SPACE_NAME + ``` + +## Step 3: Prepare Files (Already Done!) + +I have already configured the necessary files for you: +* **`Dockerfile`**: Builds the React frontend and sets up the Python backend. +* **`app.py`**: Configured to run on port 7860 (required by HF Spaces). +* **`requirements.txt`**: Lists all Python dependencies. + +## Step 4: Deploy + +To deploy, simply commit your changes and push to the `space` remote. + +1. **Add files**: + ```bash + git add . + ``` +2. **Commit**: + ```bash + git commit -m "Initial deploy to Hugging Face" + ``` +3. **Push**: + ```bash + git push space master:main + ``` + *(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`)*. + + > **IMPORTANT: Authentication** + > When asked for your **Username**, enter your Hugging Face username. + > When asked for your **Password**, you MUST enter an **Access Token**, not your account password. + > + > **How to get a Token:** + > 1. Go to [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens). + > 2. Click **Create new token**. + > 3. Type: **Write**. Name: "Deploy". + > 4. Copy the token (starts with `hf_...`). + > 5. Paste this token when the terminal asks for your password. + > + > **Troubleshooting: "Updates were rejected"** + > If you see an error saying "Updates were rejected", it means your Space already has a `README.md` that you don't have locally. + > For the **first push only**, you can force overwrite it: + > ```bash + > git push --force space master:main + > ``` + > + > **Troubleshooting: "File larger than 10MB"** + > If you see an error about `static/background.mp3` being too large, you need **Git LFS**: + > ```bash + > git lfs install + > git lfs track "static/background.mp3" + > git add .gitattributes + > git commit --amend --no-edit + > git push --force space master:main + > ``` + +## Step 5: Future Updates + +To reflect future changes on the live site: + +1. Make your changes in the code. +2. Run: + ```bash + git add . + git commit -m "Update description" + git push space master:main + ``` + +The Space will automatically rebuild and update! + +## Troubleshooting + +* **Build Failures**: Check the "Logs" tab in your Hugging Face Space to see why the build failed. +* **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. diff --git a/frontend/App.tsx b/frontend/App.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0c4d34d9e575516a05e5d023ca53512c5895cb5d --- /dev/null +++ b/frontend/App.tsx @@ -0,0 +1,128 @@ +import React from 'react'; +import { BrowserRouter as Router, Routes, Route, Navigate, Link } from 'react-router-dom'; +import Hero from './components/Hero'; +import SingleAnalysis from './components/SingleAnalysis'; +import BatchAnalysis from './components/BatchAnalysis'; + +const App: React.FC = () => { + const [isPlaying, setIsPlaying] = React.useState(false); + const audioRef = React.useRef(null); + + React.useEffect(() => { + const audio = audioRef.current; + if (!audio) return; + + audio.volume = 0.5; + + // 1. Try to autoplay immediately + const playPromise = audio.play(); + if (playPromise !== undefined) { + playPromise + .then(() => setIsPlaying(true)) + .catch((error) => { + console.log("Autoplay blocked. Waiting for user interaction.", error); + setIsPlaying(false); + }); + } + + // 2. Add a global click listener as a fallback + // As soon as the user clicks ANYWHERE, we start the audio + const handleUserInteraction = () => { + if (audio.paused) { + audio.play() + .then(() => { + setIsPlaying(true); + // Remove listener once successful + document.removeEventListener('click', handleUserInteraction); + }) + .catch(e => console.error("Play failed even after interaction:", e)); + } + }; + + document.addEventListener('click', handleUserInteraction); + + return () => { + document.removeEventListener('click', handleUserInteraction); + }; + }, []); + + const toggleAudio = () => { + if (audioRef.current) { + if (isPlaying) { + audioRef.current.pause(); + } else { + audioRef.current.play(); + } + setIsPlaying(!isPlaying); + } + }; + + return ( + +
+ {/* Global Nav / Logo - Fixed and High Z-Index */} +
+ + {/* Logo Icon */} +
+ Logo +
+ Samsung Prism Prototype + + + {/* Audio Toggle */} +
+ {!isPlaying && ( +
+ {/* Gradient Border Container */} +
+ {/* Inner Glass Content */} +
+ + ✨ Don't miss the magic! 🎧 + +
+ {/* Arrow pointing to button */} +
+
+
+ )} + +
+
+ +
+ + ); +}; + +export default App; \ No newline at end of file diff --git a/frontend/components/BackgroundAnimation.tsx b/frontend/components/BackgroundAnimation.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d429036c10ed6c860dfa76b82e65e278729c378f --- /dev/null +++ b/frontend/components/BackgroundAnimation.tsx @@ -0,0 +1,103 @@ +import React, { useEffect, useRef } from 'react'; + +const BackgroundAnimation: React.FC = () => { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + let width = window.innerWidth; + let height = window.innerHeight; + canvas.width = width; + canvas.height = height; + + // Star parameters + const numStars = 400; + const speed = 2; // Speed of travel + const stars: { x: number; y: number; z: number; o: number }[] = []; + + // Initialize stars + for (let i = 0; i < numStars; i++) { + stars.push({ + x: Math.random() * width - width / 2, + y: Math.random() * height - height / 2, + z: Math.random() * width, // Depth + o: Math.random(), // Original z for resetting + }); + } + + const animate = () => { + // Clear screen with a slight fade trail for motion blur effect (optional, using clearRect for crispness now) + ctx.fillStyle = '#020617'; // Match slate-950 + ctx.fillRect(0, 0, width, height); + + const cx = width / 2; + const cy = height / 2; + + stars.forEach((star) => { + // Move star closer + star.z -= speed; + + // Reset if it passes the screen + if (star.z <= 0) { + star.z = width; + star.x = Math.random() * width - width / 2; + star.y = Math.random() * height - height / 2; + } + + // Project 3D to 2D + // The factor 'width / star.z' makes things bigger as they get closer (z decreases) + const x = cx + (star.x / star.z) * width; + const y = cy + (star.y / star.z) * width; + + // Calculate size based on proximity + const size = (1 - star.z / width) * 3; + + // Calculate opacity based on proximity (fade in as they appear) + const opacity = (1 - star.z / width); + + // Draw star + if (x >= 0 && x <= width && y >= 0 && y <= height) { + ctx.beginPath(); + ctx.fillStyle = `rgba(255, 255, 255, ${opacity})`; + ctx.arc(x, y, size, 0, Math.PI * 2); + ctx.fill(); + } + }); + + requestAnimationFrame(animate); + }; + + const animationId = requestAnimationFrame(animate); + + const handleResize = () => { + width = window.innerWidth; + height = window.innerHeight; + canvas.width = width; + canvas.height = height; + }; + + window.addEventListener('resize', handleResize); + + return () => { + cancelAnimationFrame(animationId); + window.removeEventListener('resize', handleResize); + }; + }, []); + + return ( +
+ + + {/* Subtle Nebula Overlay for atmosphere */} +
+
+
+ ); +}; + +export default BackgroundAnimation; diff --git a/frontend/components/BatchAnalysis.tsx b/frontend/components/BatchAnalysis.tsx new file mode 100644 index 0000000000000000000000000000000000000000..49f19597ae301c97537d8414cc240782837f86e7 --- /dev/null +++ b/frontend/components/BatchAnalysis.tsx @@ -0,0 +1,389 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { UploadIcon, StackIcon, DownloadIcon, ArrowLeftIcon, CheckCircleIcon, XCircleIcon } from './Icons'; +import { BatchItem } from '../types'; +import { uploadMultiple, classifyMultipleStream, clearUploads, getSamples, useSample } from '../services/apiService'; + +const BatchAnalysis: React.FC = () => { + const navigate = useNavigate(); + const [items, setItems] = useState([]); + const [processing, setProcessing] = useState(false); + const [showSamples, setShowSamples] = useState(false); + const [samples, setSamples] = useState<{ id: number, path: string, name: string }[]>([]); + const fileInputRef = useRef(null); + + useEffect(() => { + const fetchSamples = async () => { + try { + const data = await getSamples(); + if (Array.isArray(data)) { + setSamples(data); + } + } catch (err) { + console.error("Failed to fetch samples", err); + } + }; + fetchSamples(); + }, []); + + const handleFileChange = async (e: React.ChangeEvent) => { + if (e.target.files && e.target.files.length > 0) { + const newFiles = Array.from(e.target.files) as File[]; + + // Create preview items + const newItems: BatchItem[] = newFiles.map(file => ({ + id: Math.random().toString(36).substr(2, 9), + file: file, + previewUrl: URL.createObjectURL(file), + status: 'pending' + })); + + setItems(prev => [...prev, ...newItems]); + + // Upload files immediately + try { + await uploadMultiple(newFiles); + } catch (err) { + console.error("Upload failed", err); + // Mark these items as error + setItems(prev => prev.map(item => + newItems.find(ni => ni.id === item.id) ? { ...item, status: 'error' } : item + )); + } + } + }; + + const addSampleToQueue = async (filename: string, url: string) => { + try { + // Call backend to copy sample + await useSample(filename, 'multiple'); + + // Create a dummy file object for UI state consistency + // The backend already has the file, so we don't need actual content here + const file = new File([""], filename, { type: "image/png" }); + + const newItem: BatchItem = { + id: Math.random().toString(36).substr(2, 9), + file, + previewUrl: url, + status: 'pending' + }; + + setItems(prev => [...prev, newItem]); + + } catch (err) { + console.error("Failed to load sample", err); + } + }; + + const normalizeFilename = (name: string) => { + // Basic emulation of werkzeug.secure_filename behavior + // 1. ASCII only (remove non-ascii) - simplified here to just keep standard chars + // 2. Replace whitespace with underscore + // 3. Remove invalid chars + let normalized = name.replace(/\s+/g, '_'); + normalized = normalized.replace(/[^a-zA-Z0-9._-]/g, ''); + return normalized; + }; + + const runBatchProcessing = async () => { + setProcessing(true); + setItems(prev => prev.map(item => ({ ...item, status: 'processing', error: undefined }))); + + try { + // Use the generator helper which handles buffering and parsing correctly + for await (const result of classifyMultipleStream()) { + console.log("Received result:", result); + + if (result.error) { + console.error("Error for file:", result.filename, result.error); + setItems(prev => prev.map(item => { + // Check exact match or normalized match + if (item.file.name === result.filename || normalizeFilename(item.file.name) === result.filename) { + return { ...item, status: 'error', error: result.error }; + } + return item; + })); + continue; + } + + setItems(prev => prev.map(item => { + // Check exact match or normalized match + if (item.file.name === result.filename || normalizeFilename(item.file.name) === result.filename) { + return { + ...item, + status: 'completed', + result: result.status === 'pass' ? 'pass' : 'fail', + labels: result.labels + }; + } + return item; + })); + } + + } catch (err) { + console.error("Batch processing error:", err); + setItems(prev => prev.map(item => + item.status === 'processing' ? { ...item, status: 'error', error: 'Network or server error' } : item + )); + } finally { + setProcessing(false); + // Safety check: Mark any remaining processing items as error + setItems(prev => prev.map(item => + item.status === 'processing' ? { + ...item, + status: 'error', + error: 'No result from server (Filename mismatch or timeout)' + } : item + )); + } + }; + + const getProgress = () => { + if (items.length === 0) return 0; + const completed = items.filter(i => i.status === 'completed' || i.status === 'error').length; + return (completed / items.length) * 100; + }; + + const downloadReport = () => { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const htmlContent = ` + + + + Prism Batch Report - ${timestamp} + + + +

Batch Classification Report

+

Generated on: ${new Date().toLocaleString()}

+ + + + + + + + + + + ${items.map(item => ` + + + + + + + `).join('')} + +
FilenameStatusResultFailure Reason
${item.file.name}${item.status}${item.result ? item.result.toUpperCase() : '-'}${item.labels && item.labels.length > 0 ? `${item.labels.join(', ')}` : '-'}
+ + + `; + + const blob = new Blob([htmlContent], { type: 'text/html' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `prism-batch-report-${timestamp}.html`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const clearAll = async () => { + setItems([]); + await clearUploads(); + }; + + const isComplete = items.length > 0 && items.every(i => i.status === 'completed' || i.status === 'error'); + + return ( +
+
+

Batch Image Analysis

+
+ + {/* Controls */} +
+
+
+ + + + {items.length > 0 && ( + + )} +
+ +
+
+
+
+ {Math.round(getProgress())}% +
+
+ + {/* Sample Gallery Toggle */} + + +
+
+
+ {samples.map((sample) => { + const isSelected = items.some(item => item.previewUrl === sample.url); + return ( +
addSampleToQueue(sample.filename, sample.url)} + > + {`Sample +
+ {isSelected && ( +
+ +
+ )} +
+
+ ); + })} +
+
+
+
+ + {/* Status Bar */} + {items.length > 0 && ( +
+
+

{items.length} items in queue

+ {processing && ( +

+ Running on CPU: Classification takes time, please be patient 🐨✨ +

+ )} +
+
+ + +
+
+ )} + + {/* Grid */} +
+ {items.map((item) => ( +
+ Batch Item + + {/* Overlay Status */} +
+ {item.status === 'processing' && ( + ANALYZING... + )} + {item.status === 'pending' && ( + PENDING + )} + {item.status === 'error' && ( +
+ ERROR + {item.error && ( + + {item.error.length > 50 ? item.error.substring(0, 50) + '...' : item.error} + + )} +
+ )} + {item.status === 'completed' && ( +
+
+ {item.result === 'pass' + ? + : + } + + {item.result} + +
+ {item.labels && item.labels.length > 0 && ( +
+ {item.labels.map((label, idx) => ( + + {label} + + ))} +
+ )} +
+ )} +
+
+ ))} +
+
+ ); +}; + +export default BatchAnalysis; \ No newline at end of file diff --git a/frontend/components/Hero.tsx b/frontend/components/Hero.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c84d267d03ceaae0c71dafe38829437741b30693 --- /dev/null +++ b/frontend/components/Hero.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { ImageIcon, StackIcon } from './Icons'; +import BackgroundAnimation from './BackgroundAnimation'; + +const Hero: React.FC = () => { + const navigate = useNavigate(); + + return ( +
+ + + +
+ +
+
+ + v2.0 Prototype +
+ +

+ PRISM +

+

+ Lock Screen Classifier +

+

+ Automated Compliance for Samsung Glance +

+
+ +
+

+ "Making classification of lock screens generated by Glance for Samsung + automatic without human intervention, + saving 40 hr/week bandwidth." +

+
+ +
+ + + +
+
+ + {/* Proof of Work Section - Actual Images */} +
+
+

Proof of Work

+
+
+ +
+ {/* Certificate 1: Participation (Top) */} +
+
+
+ Participation Certificate { + (e.target as HTMLImageElement).src = 'https://placehold.co/800x600/1e293b/FFF?text=Upload+Participation+Cert+to+static/certificates/'; + }} + /> +
+
+ + {/* Certificate 2: Excellence (Bottom) */} +
+
+
+ Certificate of Excellence { + (e.target as HTMLImageElement).src = 'https://placehold.co/800x600/1e293b/FFF?text=Upload+Excellence+Cert+to+static/certificates/'; + }} + /> +
+
+
+
+ +
+

© made by Devansh Singh

+
+
+ ); +}; + +export default Hero; diff --git a/frontend/components/Icons.tsx b/frontend/components/Icons.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f4dabf40316dd41817dc3f48af17ec1682bf2ce7 --- /dev/null +++ b/frontend/components/Icons.tsx @@ -0,0 +1,64 @@ +import React from 'react'; + +export const UploadIcon = () => ( + + + + + +); + +export const ImageIcon = () => ( + + + + + +); + +export const StackIcon = () => ( + + + + + +); + +export const CheckCircleIcon = ({ className }: { className?: string }) => ( + + + + +); + +export const XCircleIcon = ({ className }: { className?: string }) => ( + + + + + +); + +export const ArrowLeftIcon = () => ( + + + + +); + +export const ScanIcon = () => ( + + + + + + +); + +export const DownloadIcon = () => ( + + + + + +); \ No newline at end of file diff --git a/frontend/components/SingleAnalysis.tsx b/frontend/components/SingleAnalysis.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0a92a9a4d06308a8f9280e499ac4f7ef0a398690 --- /dev/null +++ b/frontend/components/SingleAnalysis.tsx @@ -0,0 +1,253 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { CheckCircleIcon, XCircleIcon, UploadIcon, ScanIcon, ArrowLeftIcon } from './Icons'; +import { SingleAnalysisReport } from '../types'; +import { uploadSingle, classifySingle, getSamples, useSample } from '../services/apiService'; + +interface SingleAnalysisProps { + onBack: () => void; +} + +const SingleAnalysis: React.FC = () => { + const navigate = useNavigate(); + const [image, setImage] = useState(null); + const [preview, setPreview] = useState(null); + const [loading, setLoading] = useState(false); + const [report, setReport] = useState(null); + const [samples, setSamples] = useState<{ id: number, url: string, filename: string }[]>([]); + const fileInputRef = useRef(null); + + useEffect(() => { + const fetchSamples = async () => { + try { + const data = await getSamples(); + if (Array.isArray(data)) { + setSamples(data); + } + } catch (err) { + console.error("Failed to fetch samples", err); + } + }; + fetchSamples(); + }, []); + + const handleFileChange = (e: React.ChangeEvent) => { + if (e.target.files && e.target.files[0]) { + const file = e.target.files[0]; + setImage(file); + setPreview(URL.createObjectURL(file)); + setReport(null); + } + }; + + const runClassification = async (filename: string) => { + setLoading(true); + try { + const result = await classifySingle(filename); + setReport(result); + } catch (err) { + console.error(err); + alert("Analysis failed. Please try again."); + } finally { + setLoading(false); + } + }; + + const handleUploadAndAnalyze = async () => { + if (!image) return; + setLoading(true); + try { + const filename = await uploadSingle(image); + await runClassification(filename); + } catch (err) { + console.error(err); + alert("Upload failed."); + setLoading(false); + } + }; + + const handleSampleSelect = async (filename: string) => { + setLoading(true); + try { + // Call backend to copy sample to uploads folder + await useSample(filename, 'single'); + + // Set preview + setPreview(`/static/samples/${filename}`); + setImage(null); // Clear file input + + // Run classification on the sample (now in uploads folder) + await runClassification(filename); + } catch (err) { + console.error("Failed to use sample", err); + alert("Failed to load sample."); + setLoading(false); + } + }; + + const reset = () => { + setImage(null); + setPreview(null); + setReport(null); + }; + + return ( +
+
+

Single Image Analysis

+
+ +
+ {/* Left: Upload / Preview */} +
+
+ + + {preview ? ( +
+ Preview + +
+ ) : ( +
fileInputRef.current?.click()} + 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" + > +
+ +
+

Upload Image

+

Drag & drop or click to browse

+

Supports PNG, JPG, JPEG

+
+ )} +
+ + {/* Sample Gallery */} +
+

Or try a sample

+
+ {samples.map((sample) => { + const isSelected = preview === sample.url; + return ( +
handleSampleSelect(sample.filename)} + > + {`Sample +
+ {isSelected && ( +
+ +
+ )} +
+
+ ); + })} +
+
+
+ + {/* Right: Report Area */} +
+ {!report && ( +
+
+ +
+

Upload an image or select a sample to generate a compliance report.

+ + {image && !loading && ( + + )} +
+ )} + + {report && ( +
+
+

Compliance Report

+
+ AI Verified +
+
+ + {/* DEBUG: Check what we are receiving */} + {/*
{JSON.stringify(report, null, 2)}
*/} + + {/* Render Tailwind Table */} +
+ + + + + + + + + {report.detailed_results && report.detailed_results.map(([label, result], index) => { + const isFail = String(result).startsWith('1') || result === 1; + return ( + + + + + ); + })} + +
LabelResult
{label} + + {isFail ? 'Fail' : 'Pass'} + +
+
+ +
+ +
+
+ )} +
+
+
+ ); +}; + +export default SingleAnalysis; \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000000000000000000000000000000000000..817dad745a150c0e16d25d4bc21515db288a1f5c --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,190 @@ + + + + + + + + Prototype v2.0 + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/frontend/index.tsx b/frontend/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6ca5361e60877dce8e4016f9b023df96c5183d07 --- /dev/null +++ b/frontend/index.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error("Could not find root element to mount to"); +} + +const root = ReactDOM.createRoot(rootElement); +root.render( + + + +); \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..9c4379deb203aa665687acf603d3f28b5c81bde2 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2441 @@ +{ + "name": "prism---lock-screen-classifier", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "prism---lock-screen-classifier", + "version": "0.0.0", + "dependencies": { + "@google/genai": "^1.30.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.9.6" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.30.0.tgz", + "integrity": "sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w==", + "dependencies": { + "google-auth-library": "^10.3.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.20.1" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.47", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz", + "integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "engines": { + "node": "*" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001756", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz", + "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.259", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz", + "integrity": "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", + "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz", + "integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==", + "dependencies": { + "react-router": "7.9.6" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000000000000000000000000000000000000..f4e149f0989e87a78436d1fd7e59e5d8516184a5 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "prism---lock-screen-classifier", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@google/genai": "^1.30.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.9.6" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} diff --git a/frontend/public/favicon.png b/frontend/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..d9e73d4eeb952cf063de6595e1b9b0b3f7c57a68 --- /dev/null +++ b/frontend/public/favicon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a9f99048a3325c0a9c15bcb783495e9f373467d38181e448540515b65cf6019 +size 469309 diff --git a/frontend/services/apiService.ts b/frontend/services/apiService.ts new file mode 100644 index 0000000000000000000000000000000000000000..18ca0109df3931dcdaabbf21a613e2a27f1d37e7 --- /dev/null +++ b/frontend/services/apiService.ts @@ -0,0 +1,141 @@ +import { SingleAnalysisReport, BatchStreamResult } from "../types"; + +/** + * Uploads a single file to the Flask backend. + */ +export const uploadSingle = async (file: File): Promise => { + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch('/upload_single', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + throw new Error(`Upload failed: ${response.statusText}`); + } + + const data = await response.json(); + return data.filename; +}; + +/** + * Triggers classification for a single image by filename. + * Expects the backend to return { result_table: "..." } + */ +export const classifySingle = async (filename: string): Promise => { + const response = await fetch('/classify_single', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ filename }), + }); + + if (!response.ok) { + throw new Error(`Classification failed: ${response.statusText}`); + } + + const data = await response.json(); + return { + classification: data.classification, + detailed_results: data.detailed_results, + html: data.result_table // Optional fallback + }; +}; + +/** + * Uploads multiple files for batch processing. + */ +export const uploadMultiple = async (files: File[]): Promise => { + const formData = new FormData(); + files.forEach(file => { + formData.append('file', file); + }); + + const response = await fetch('/upload_multiple', { + method: 'POST', + body: formData, + }); + + if (!response.ok) { + throw new Error(`Batch upload failed: ${response.statusText}`); + } + // Assuming success means files are ready for classification +}; + +/** + * Triggers batch classification and returns the raw response for manual streaming. + */ +export const classifyMultiple = async (): Promise> => { + const response = await fetch('/classify_multiple', { + method: 'POST', + }); + + if (!response.ok || !response.body) { + throw new Error(`Batch classification failed: ${response.statusText}`); + } + + return response.body; +}; + +/** + * Clears all uploaded files from the backend. + */ +export const clearUploads = async () => { + const response = await fetch('/clear_uploads', { + method: 'POST', + }); + return response.json(); +}; + +export const getSamples = async () => { + const response = await fetch('/api/samples'); + return response.json(); +}; + +export const useSample = async (filename: string, destination: 'single' | 'multiple') => { + const response = await fetch('/api/use_sample', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filename, destination }) + }); + return response.json(); +}; + +/** + * Triggers batch classification and yields results as they stream in. + */ +export async function* classifyMultipleStream(): AsyncGenerator { + const stream = await classifyMultiple(); + const reader = stream.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + + // Process lines (assuming NDJSON or similar line-delimited JSON) + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; // Keep incomplete line in buffer + + for (const line of lines) { + if (line.trim()) { + try { + const result = JSON.parse(line); + yield result as BatchStreamResult; + } catch (e) { + console.warn("Failed to parse stream chunk", e); + } + } + } + } + } finally { + reader.releaseLock(); + } +} \ No newline at end of file diff --git a/frontend/services/geminiService.ts b/frontend/services/geminiService.ts new file mode 100644 index 0000000000000000000000000000000000000000..babe48a58b61cc7a725e83759ce7909fed9e7dbf --- /dev/null +++ b/frontend/services/geminiService.ts @@ -0,0 +1,96 @@ +import { GoogleGenAI, Type, Schema } from "@google/genai"; +import { AnalysisReport } from "../types"; + +// Initialize Gemini Client +// Note: API Key is injected via process.env.API_KEY +const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); + +const MODEL_NAME = "gemini-2.5-flash"; + +const analysisSchema: Schema = { + type: Type.OBJECT, + properties: { + isCompliant: { type: Type.BOOLEAN, description: "Whether the image is suitable for a public lock screen." }, + overallScore: { type: Type.INTEGER, description: "A quality score from 0 to 100." }, + checks: { + type: Type.ARRAY, + items: { + type: Type.OBJECT, + properties: { + name: { type: Type.STRING, description: "The name of the criterion being checked." }, + passed: { type: Type.BOOLEAN, description: "Whether the check passed." }, + reason: { type: Type.STRING, description: "Brief explanation of the result." } + }, + required: ["name", "passed", "reason"] + } + } + }, + required: ["isCompliant", "overallScore", "checks"] +}; + +/** + * Converts a File object to a Base64 string for the API. + */ +const fileToGenerativePart = async (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => { + const base64String = reader.result as string; + // Remove data url prefix (e.g. "data:image/jpeg;base64,") + const base64Data = base64String.split(',')[1]; + resolve(base64Data); + }; + reader.onerror = reject; + reader.readAsDataURL(file); + }); +}; + +export const analyzeLockScreen = async (file: File): Promise => { + try { + const base64Data = await fileToGenerativePart(file); + + const systemPrompt = ` + You are Prism, an AI expert for Samsung Glance lock screen compliance. + Analyze the provided image against the following strict criteria: + 1. Image Quality: Must be high resolution, not blurry, no artifacts. + 2. Ribbon Detection: Ensure no promotional ribbons, watermarks, or text overlays covering the subject. + 3. Text Legibility: If there is text, is it legible? (Prefer no text for wallpapers). + 4. Safe Content: No offensive, violent, or adult content. + 5. Subject Centering: The main subject should be well-positioned for a mobile lock screen (portrait aspect). + + Return the result as a structured JSON object. + `; + + const response = await ai.models.generateContent({ + model: MODEL_NAME, + contents: { + parts: [ + { inlineData: { mimeType: file.type, data: base64Data } }, + { text: systemPrompt } + ] + }, + config: { + responseMimeType: "application/json", + responseSchema: analysisSchema, + temperature: 0.2, // Low temperature for consistent, objective analysis + } + }); + + if (response.text) { + return JSON.parse(response.text) as AnalysisReport; + } else { + throw new Error("No response text from Gemini."); + } + + } catch (error) { + console.error("Gemini Analysis Failed:", error); + // Fallback mock error for UI stability if API fails completely + return { + isCompliant: false, + overallScore: 0, + checks: [ + { name: "System Error", passed: false, reason: "Failed to connect to AI service." } + ] + }; + } +}; \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..2c6eed55868c7545e8f265f260277fb0605b2dbc --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + "types": [ + "node" + ], + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "allowJs": true, + "jsx": "react-jsx", + "paths": { + "@/*": [ + "./*" + ] + }, + "allowImportingTsExtensions": true, + "noEmit": true + } +} \ No newline at end of file diff --git a/frontend/types.ts b/frontend/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..85f766bb72130a0810361f161facd71e9bc35f16 --- /dev/null +++ b/frontend/types.ts @@ -0,0 +1,43 @@ + +export interface CheckResult { + name: string; + passed: boolean; + reason: string; +} + +// Single analysis returns HTML string in result_table +export interface SingleAnalysisReport { + classification: string; + detailed_results: [string, string | number][]; + html?: string; // Keeping for backward compatibility if needed, but optional +} + +// Added AnalysisReport for geminiService +export interface AnalysisReport { + isCompliant: boolean; + overallScore: number; + checks: CheckResult[]; +} + +// Batch analysis streams JSON updates +export interface BatchStreamResult { + filename: string; + status: 'pass' | 'fail' | 'error'; + score?: number; + details?: string; + labels?: string[]; + error?: string; +} + +export interface BatchItem { + id: string; + file: File; + previewUrl: string; + status: 'pending' | 'processing' | 'completed' | 'error'; + result?: 'pass' | 'fail'; + score?: number; + labels?: string[]; + error?: string; +} + +export type ViewState = 'home' | 'single' | 'batch'; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..74805c04e5ef168f1e88e3e44d347ee1187922f9 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,28 @@ +import path from 'path'; +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, '.', ''); + return { + build: { + outDir: '../static/react', + emptyOutDir: true, + }, + base: '/static/react/', + server: { + port: 3000, + host: '0.0.0.0', + }, + plugins: [react()], + define: { + 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + } + } + }; +}); diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..913319691224dfdfea64fe42e059cb57469c2dd5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +easyocr +fuzzywuzzy +emoji +torch +torchvision +transformers + +pillow +opencv-python-headless +tabulate +nltk +einops +timm +accelerate +flask +python-Levenshtein +numpy<2 \ No newline at end of file diff --git a/static/certificates/excellence.png b/static/certificates/excellence.png new file mode 100644 index 0000000000000000000000000000000000000000..df44fa4824b51f2548b753690db800bd7983f67e --- /dev/null +++ b/static/certificates/excellence.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b84a2aafee602030eafeae8343d0c3cab0409ab750328e8365a728a079c56731 +size 231888 diff --git a/static/certificates/participation.png b/static/certificates/participation.png new file mode 100644 index 0000000000000000000000000000000000000000..83a01ab92c0fc99d449afd944e032a135284b62e --- /dev/null +++ b/static/certificates/participation.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b11ded2481197c22e72796ccdfbe8b567516b9c24c5c5ff9589640fb4ee077d9 +size 207027 diff --git a/static/logo.png b/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..42b78fe166c429a0d48c827adb0e1852b30b0c32 --- /dev/null +++ b/static/logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0516322c781b87e75c32f1253982ea10608265617cf47e9334f1ccaba3ae3a07 +size 24258 diff --git a/static/samples/1.png b/static/samples/1.png new file mode 100644 index 0000000000000000000000000000000000000000..900fab62f037233592a53a6126963d502429788c --- /dev/null +++ b/static/samples/1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58cb5ed4ada4f6a5d4991c9c4fb77311490184a0ea83926b37b7897e3421ab42 +size 2757026 diff --git a/static/samples/Angry husband trying to kill his wife indoors_ Concept of domestic violence.jpg b/static/samples/Angry husband trying to kill his wife indoors_ Concept of domestic violence.jpg new file mode 100644 index 0000000000000000000000000000000000000000..11bafbc9fa5fbd33e2f1592bbfb8013645880a56 --- /dev/null +++ b/static/samples/Angry husband trying to kill his wife indoors_ Concept of domestic violence.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83afb93f634f3589555e9f3b8cbdbeee5bdd971b55ad49d43ab19be34a00537c +size 26266 diff --git a/static/samples/Custom Bloodied Scream Buck 120 Knife Prop (1).jpg b/static/samples/Custom Bloodied Scream Buck 120 Knife Prop (1).jpg new file mode 100644 index 0000000000000000000000000000000000000000..0db1ef2ebcfa44161333a462ede10826b64b8341 --- /dev/null +++ b/static/samples/Custom Bloodied Scream Buck 120 Knife Prop (1).jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67d004ed79d9aa97a0a676b6d9d0dc8bf1c31cedeb8fab37a8b581e84d56bed3 +size 127653 diff --git a/static/samples/NoTag2.jpg b/static/samples/NoTag2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ebdfb7ef0d880a3f6f87cca325ca45fab5ce68fc --- /dev/null +++ b/static/samples/NoTag2.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a44a5de577758b462e55a17df1c3f649b721d9598a4ce74342a352b32df42a7d +size 109064 diff --git a/static/samples/Picture4.png b/static/samples/Picture4.png new file mode 100644 index 0000000000000000000000000000000000000000..20182f1152188acf18efcec7a7a05ec01cd6dd1b --- /dev/null +++ b/static/samples/Picture4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:28c98daedab262947b9ed3f1bed0cc1b64833b462ad1b635769fdbd768fec692 +size 179175 diff --git a/static/samples/Screenshot_20241020_142718_One UI Home.jpg b/static/samples/Screenshot_20241020_142718_One UI Home.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3ea2be2367865a5c255b1d70b245b9118c219a64 --- /dev/null +++ b/static/samples/Screenshot_20241020_142718_One UI Home.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ac5bfc0fb37b327138c7d300e70492b686ef8c0b4e79d20042cf189210976fb +size 837034 diff --git a/static/samples/Screenshot_20241021-142831_One UI Home.jpg b/static/samples/Screenshot_20241021-142831_One UI Home.jpg new file mode 100644 index 0000000000000000000000000000000000000000..047e08f5336aa8bebdbf26c6b3d5ac8cc7d1ea37 --- /dev/null +++ b/static/samples/Screenshot_20241021-142831_One UI Home.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:708f5057e31348663bf5692f05a9473d1a7afba4d54fe8c6320b76d50cee0cdd +size 695482 diff --git a/static/samples/Screenshot_20241022-125250_One UI Home.jpg b/static/samples/Screenshot_20241022-125250_One UI Home.jpg new file mode 100644 index 0000000000000000000000000000000000000000..78bd7d59f6a6cfe13885ff87c255ca32beaacea6 --- /dev/null +++ b/static/samples/Screenshot_20241022-125250_One UI Home.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a5456f995857cdcaee700818753295d75a21172ba0b91e8b185ac913164b653e +size 972368 diff --git a/static/samples/Screenshot_20241022-133424_One UI Home.jpg b/static/samples/Screenshot_20241022-133424_One UI Home.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3c617cb69c597ad4df511439ab655910446455a8 --- /dev/null +++ b/static/samples/Screenshot_20241022-133424_One UI Home.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f130f6d724b0c4e8a4ac94daf2d7af222933cea09e0bdbf2047208d7294b3a32 +size 1113488 diff --git a/static/samples/conf2.jpg b/static/samples/conf2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f1f429012a014efcb27afa7edf365a41ac19e58a --- /dev/null +++ b/static/samples/conf2.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e46d0333423e8d4a977f8a5f4f92ccaf2ce0d553d38cdc026f0e20cc6cea10a8 +size 814046 diff --git a/static/samples/natraj.jpg b/static/samples/natraj.jpg new file mode 100644 index 0000000000000000000000000000000000000000..04f3cab80bc4652863304c018afc3b1b634617ba --- /dev/null +++ b/static/samples/natraj.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2943f0f0dfa6cfa48d16c9198809d894ccd86ccec855666c11f7163839bfa9ba +size 214605 diff --git a/static/samples/notEng.jpeg b/static/samples/notEng.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..2f4495c6f09a2606bb2c062ee93b05a166da1881 --- /dev/null +++ b/static/samples/notEng.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67bdaa193af8a76bc411da7bd6ff85291f96a16730412de367cb723c925ed7cb +size 409859 diff --git a/static/samples/pixelcut-export (1).jpeg b/static/samples/pixelcut-export (1).jpeg new file mode 100644 index 0000000000000000000000000000000000000000..0facc4b0ad01f8a8ad2c5fe09e304ae563ce27c8 --- /dev/null +++ b/static/samples/pixelcut-export (1).jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9f5d996dd7016bec255f72485c301550f8ba2dc8cbeabf52dedce14ace5b8b9 +size 236954 diff --git a/static/samples/pixelcut-export (2).png b/static/samples/pixelcut-export (2).png new file mode 100644 index 0000000000000000000000000000000000000000..4d84b9403db62f58b1bd61f5871174a65b78be36 --- /dev/null +++ b/static/samples/pixelcut-export (2).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5860d5eb958da632c1ae794f439f92f1d564712a8ebd1686abfd5a956823bd7 +size 1987247 diff --git a/static/samples/pixelcut-export (3).jpeg b/static/samples/pixelcut-export (3).jpeg new file mode 100644 index 0000000000000000000000000000000000000000..e2ac0c014fe4b4138492461ea199ef5ef28bcc93 --- /dev/null +++ b/static/samples/pixelcut-export (3).jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abb943dd5dabc2ce29eee05355cf96965d643f4f8f273a8d621f872ec89d2f70 +size 233401 diff --git a/static/samples/pixelcut-export (3).png b/static/samples/pixelcut-export (3).png new file mode 100644 index 0000000000000000000000000000000000000000..59d81d60c71dd46b17a3e81649a07af53c4027c7 --- /dev/null +++ b/static/samples/pixelcut-export (3).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3e43844606454df46c89689b233408eeeebffef5373cf1567f4c918e2430be5 +size 2097656 diff --git a/static/samples/pixelcut-export (5).jpeg b/static/samples/pixelcut-export (5).jpeg new file mode 100644 index 0000000000000000000000000000000000000000..22da0e911a2ae1101deeec62cb862a377b32e805 --- /dev/null +++ b/static/samples/pixelcut-export (5).jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8405b8c1d7e507d5857f044371de03021189e2833d537cef429d89e21af0558d +size 182727 diff --git a/static/samples/pixelcut-export (8).png b/static/samples/pixelcut-export (8).png new file mode 100644 index 0000000000000000000000000000000000000000..ae0b8e78049b4e7ec81d3f97ac328baee8110b6c --- /dev/null +++ b/static/samples/pixelcut-export (8).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d502eb357ebe1c62e59b9d6be945f4c010b9f2e73bf55c2c586e32464576ed0 +size 933795 diff --git a/static/samples/pixelcut-export.png b/static/samples/pixelcut-export.png new file mode 100644 index 0000000000000000000000000000000000000000..45fcfc2196a708fca16c34c0f1e48633479e8e66 --- /dev/null +++ b/static/samples/pixelcut-export.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:abe0983b60e39a2b7dc9566cc45f85d285dabd1a67730b73efc1ddb169900427 +size 1746140 diff --git a/static/samples/ppt9.jpg b/static/samples/ppt9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..44c78b1fcf08c21c81fd86d1e0da5fe077ba3cd7 --- /dev/null +++ b/static/samples/ppt9.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9a1cfcae0ac2777eac14b8d4b844e7b2f8fe1b756529e9c1da3c4269e9f144a +size 104818 diff --git a/static/samples/risky2.jpg b/static/samples/risky2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..782dcd1f47b5ca7ed83d1310fb55fc12995d6b7a --- /dev/null +++ b/static/samples/risky2.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd77ed1e35cff039449bfc2ad424ee87e8a4f9dd7b79be619d213509df0cbb8c +size 18982 diff --git a/static/samples/tnC.jpg b/static/samples/tnC.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a2e96e91bd53f9d2e8ecdf94b82d1f6401ed16f7 --- /dev/null +++ b/static/samples/tnC.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ded74eaeff97bb7f753dfae8b6b426f5229181cf64aba7071eb8cd8db01b2e22 +size 613865 diff --git a/templates/report_template.html b/templates/report_template.html new file mode 100644 index 0000000000000000000000000000000000000000..0545c819c1f8a1c5a983829c154db109f527a203 --- /dev/null +++ b/templates/report_template.html @@ -0,0 +1,283 @@ + + + + + + Classification Report + + + + + + +
+
+
+

Failed Images Report

+

Date: {date} | Time: {time}

+
+ +
+ {summary_content} +
+ +
+ + + + + + + + + + + {table_rows} + +
S.NoImageResultLabels
+
+ + +
+ +
+
+
+ +
+ +
+ + + +