diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..a6516b1fc187b9b70fead19a65645c0c523c6b59 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +.git +.gitignore +.venv +venv +env +__pycache__ +*.pyc +*.pyo +.pytest_cache +.mypy_cache +.ruff_cache +.tmp +tmp* +pytest_tmp +node_modules +frontend/node_modules +frontend/.next +frontend/out +.vscode +.idea +data +models +training +tests +*.log +.env +.env.local +.env.*.local diff --git a/.github/instructions/copilot-instructions.md b/.github/instructions/copilot-instructions.md new file mode 100644 index 0000000000000000000000000000000000000000..b4cda67aec0f29279e41ed1438a692f55171140d --- /dev/null +++ b/.github/instructions/copilot-instructions.md @@ -0,0 +1,61 @@ +# GitHub Copilot Instructions — GenAI-DeepDetect + +Read CLAUDE.md first. This file adds IDE-specific context. + +## Critical rules (never violate) + +- `src/types.py` is frozen. Never modify `EngineResult`, `DetectionResponse`, or `GeneratorLabel`. +- `NUM_GENERATOR_CLASSES = 8` everywhere. If you see 5 anywhere, fix it to 8. +- SSTGNN checkpoints load with `model.load_state_dict()` only. Never `torch.jit.load`. +- Do not use `torch.jit.script` or `torch.jit.trace` on any PyG model. +- Frontend: pages router only. No `app/` directory. No `src/` wrapper inside `frontend/`. +- All Kaggle training cells call `scripts/train_image_baseline.py`. Not `training/phase1.../train.py` directly. +- `min_detection_confidence=0.3` in all MediaPipe calls. Not 0.5. + +## Generator label index (memorise this) + +``` +0 = real 4 = dall_e +1 = unknown_gan 5 = flux +2 = stable_diffusion 6 = firefly +3 = midjourney 7 = imagen +``` + +## Path conventions + +``` +src/ library code — no sys.exit(), logging only via logging module +scripts/ Kaggle entrypoints — one file per task +training/phase*/ per-phase training scripts +tests/ mirrors src/ structure +configs/ YAML configs per engine +``` + +## Import order for training scripts + +```python +import sys +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) # repo root +from src.types import ... +from training.phaseN_*/model import ... +``` + +## Checkpoint format (all engines) + +```python +torch.save({ + "epoch": int, + "model_state": model.state_dict(), + "optimizer": optimizer.state_dict(), + "val_auc": float, + "config": dict, +}, path) +``` + +## Never do these + +- `from training.phase3_sstgnn.model import SSTGNNModel` inside `src/` — use try/except ImportError +- `torch.jit.script(sstgnn_model)` — crashes silently on PyG +- `min_detection_confidence=0.5` — fails on 40% of DFDC frames +- Hardcode `localhost:8000` in frontend — use `process.env.NEXT_PUBLIC_API_URL` +- Import `torch_geometric` at module level without a guard — not available in all envs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..d1b965747d36f5ad5531ab52a6ddcd9d93e3f168 --- /dev/null +++ b/.gitignore @@ -0,0 +1,70 @@ +# GenAI-DeepDetect .gitignore + +# ── Model files ─────────────────────────────────────────────────────────────── +models/ +*.pt +*.pth +*.bin +*.safetensors + +# ── Datasets ────────────────────────────────────────────────────────────────── +data/ +*.zip +*.tar +*.tar.gz + +# ── Cache dirs (never commit these) ────────────────────────────────────────── +.deps-local/ +.pydeps/ +.hf-cache/ +.huggingface/ +__pycache__/ +*.pyc +*.pyo +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ + +# ── Kaggle artifacts ────────────────────────────────────────────────────────── +.kaggle-kernel-output/ +.kaggle-kernel-pulled/ +kaggle.json +# Keep .kaggle-kernel/ (used for packaging) + +# ── Training logs ───────────────────────────────────────────────────────────── +training/logs/ +*.log + +# ── Environment ─────────────────────────────────────────────────────────────── +.env +.env.local +.env.*.local +venv/ +.venv/ +env/ +.env.example + +# ── IDE ─────────────────────────────────────────────────────────────────────── +.vscode/ +.idea/ +*.swp +*.swo +.DS_Store +Thumbs.db + +# ── Frontend ────────────────────────────────────────────────────────────────── +node_modules/ +frontend/node_modules/ +frontend/.next/ +frontend/out/ +frontend/.env.local +frontend/tsconfig.tsbuildinfo + +# ── Junk that got committed before ──────────────────────────────────────────── +"New Text Document.txt" +*.txt.bak + +# ── Build artifacts ─────────────────────────────────────────────────────────── +dist/ +build/ +*.egg-info/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000000000000000000000000000000000..2735dfa391e68672ac6e0abfbef073083b8c5478 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,1311 @@ +# CLAUDE.md — GenAI-DeepDetect Agent Instructions + +> Read this file before touching any code. It is the single source of truth for +> how this repo is structured, what conventions to follow, and what the hard +> constraints are. + +# CLAUDE.md — GenAI-DeepDetect + +Full implementation guide for AI-assisted development on this project. Read this +file before touching any code. + +-# CLAUDE.md — GenAI-DeepDetect + +Complete implementation guide. Read this before writing any code. All models are +**100% pre-trained** — no training required, no GPU needed locally. + +--- + +## MCP Tools — Always Use These First + +Before writing any code or looking up any API, resolve docs through MCP: + +``` +context7: resolve-library-id + query-docs + → use for: transformers, torch, mediapipe, fastapi, torch-geometric, + google-generativeai, facenet-pytorch, opencv, next.js, runpod + +huggingface: model_search + model_details + hf_doc_search + → use for: finding model cards, checking input formats, confirming + pipeline task names, verifying checkpoint sizes before using +``` + +**Rule**: Never guess an API signature. Always call `context7.query-docs` first. +Never use a HF model without calling `huggingface.model_details` to confirm it +exists, check its license, and verify its input format. + +--- + +## Project Skill And Memory Policy + +For work in this repository, always prefer the installed Claude Code skill pack +when a relevant skill applies instead of ad hoc workflows. + +- Use `context7-mcp` for any library, framework, SDK, or API question, and + before changing code that depends on external packages or hosted services. +- Use `mem-search` / claude-mem whenever the user asks about previous sessions, + prior fixes, earlier decisions, or "how we solved this before". +- When using claude-mem, scope searches to project name `genai-deepdetect` + unless the user explicitly asks for a broader search. +- Keep following the repo-specific MCP rules below even when a general-purpose + skill also applies. + +Recommended companion skills for this project: + +- `systematic-debugging` for bugs, failing tests, or unexpected runtime + behavior +- `verification-before-completion` before claiming a fix is done +- `security-review` for secrets, external APIs, uploads, and auth-sensitive + changes + +--- + +## Project Goal + +Multimodal deepfake and AI-generated content detector. + +- Input: image (JPEG/PNG/WEBP) or video (MP4/MOV/AVI, max 100MB) +- Output: `DetectionResponse` — verdict, confidence, generator attribution, + natural-language explanation, per-engine breakdown + +All inference runs on pre-trained HuggingFace checkpoints. No training scripts +need to run for the system to work. + +--- + +## Architecture + +``` +Request (image/video) + │ + ▼ + FastAPI src/api/main.py + │ + ├── FingerprintEngine (image artifacts, generator attribution) + ├── CoherenceEngine (lip-sync, biological coherence) + └── SSTGNNEngine (landmark spatio-temporal graph) + │ + ▼ + Fuser src/fusion/fuser.py + │ + ▼ + Explainer src/explainability/explainer.py ← Gemini API + │ + ▼ + DetectionResponse src/types.py +``` + +--- + +## All Pre-Trained Models + +Every model downloads via `transformers.pipeline()` or `from_pretrained()`. Zero +training. Zero fine-tuning. + +| Engine | Model | HF ID | Size | Task | +| ----------- | ------------------- | ------------------------------------------ | ------ | ---------------------- | +| Fingerprint | SDXL Detector | `Organika/sdxl-detector` | ~330MB | binary fake/real | +| Fingerprint | CLIP ViT-L/14 | `openai/clip-vit-large-patch14` | ~3.5GB | generator attribution | +| Fingerprint | AI Image Detector | `haywoodsloan/ai-image-detector-deploy` | ~90MB | ensemble backup | +| SSTGNN | DeepFake Detector | `dima806/deepfake_vs_real_image_detection` | ~100MB | ResNet50 per-frame | +| SSTGNN | Deep Fake Detector | `prithivMLmods/Deep-Fake-Detector-Model` | ~80MB | EfficientNet-B4 backup | +| Coherence | MediaPipe Face Mesh | bundled in `mediapipe` package | ~10MB | landmark extraction | +| Coherence | FaceNet VGGFace2 | `facenet-pytorch` (auto-downloads) | ~100MB | temporal embeddings | +| Coherence | SyncNet | `Junhua-Zhu/SyncNet` | ~50MB | lip-sync offset | + +CLIP is the largest at 3.5GB — preload at startup, never reload. Everything else +fits in HF Spaces 16GB RAM free tier. + +--- + +## Environment Variables + +```bash +# Required +GEMINI_API_KEY=... # Google AI Studio — free tier works +HF_TOKEN=hf_... # HuggingFace read token (free) + +# Hosting +RUNPOD_API_KEY=... # RunPod serverless (heavy video) +RUNPOD_ENDPOINT_ID=... # your deployed endpoint ID + +# Paths +MODEL_CACHE_DIR=/data/models # HF Spaces: /data/models (persists) + # local dev: /tmp/models + +# Optional +MAX_VIDEO_FRAMES=300 +MAX_VIDEO_SIZE_MB=100 +INFERENCE_BACKEND=local # "local" | "runpod" +TOKENIZERS_PARALLELISM=false +``` + +Set all secrets in: + +- HF Spaces → Settings → Repository secrets +- RunPod → Secrets tab +- Vercel → Environment Variables + +--- + +## Gemini API — Explainability Engine + +**Primary model**: `gemini-2.5-pro-preview-03-25` **Fallback model**: +`gemini-1.5-pro-002` + +Both available on Google AI Studio free tier (15 req/min, 1M tokens/day). Always +query `context7.query-docs google-generativeai GenerativeModel` before modifying +this file. + +### `src/explainability/explainer.py` + +```python +import os +import logging +import google.generativeai as genai +from src.types import EngineResult + +logger = logging.getLogger(__name__) + +genai.configure(api_key=os.environ["GEMINI_API_KEY"]) + +SYSTEM_INSTRUCTION = ( + "You are a deepfake forensics analyst writing reports for security professionals. " + "Given detection engine outputs, write exactly 2-3 sentences in plain English " + "explaining why the content is real or fake. " + "Be specific — name the strongest signals. " + "Use direct declarative sentences. No hedging. No 'I think'. " + "Output only the explanation text, nothing else." +) + +_model = None + + +def _get_model() -> genai.GenerativeModel: + global _model + if _model is None: + for name in ("gemini-2.5-pro-preview-03-25", "gemini-1.5-pro-002"): + try: + _model = genai.GenerativeModel( + model_name=name, + system_instruction=SYSTEM_INSTRUCTION, + ) + logger.info(f"Gemini model loaded: {name}") + break + except Exception as e: + logger.warning(f"Gemini {name} unavailable: {e}") + return _model + + +def explain( + verdict: str, + confidence: float, + engine_results: list[EngineResult], + generator: str, +) -> str: + breakdown = "\n".join( + f"- {r.engine}: {r.verdict} ({r.confidence:.0%}) — {r.explanation}" + for r in engine_results + ) + prompt = ( + f"Verdict: {verdict} ({confidence:.0%} confidence)\n" + f"Attributed generator: {generator}\n" + f"Engine breakdown:\n{breakdown}\n\n" + "Write the forensics explanation." + ) + try: + model = _get_model() + if model is None: + raise RuntimeError("No Gemini model available") + response = model.generate_content(prompt) + return response.text.strip() + except Exception as e: + logger.error(f"Gemini explain failed: {e}") + top = engine_results[0] if engine_results else None + return ( + f"Content classified as {verdict} with {confidence:.0%} confidence. " + f"{'Primary signal from ' + top.engine + ' engine.' if top else ''}" + ) +``` + +--- + +## Engine Implementations + +### FingerprintEngine — `src/engines/fingerprint/engine.py` + +Query context7 for `transformers pipeline image-classification` and +`huggingface model_details Organika/sdxl-detector` before modifying. + +```python +import os, logging, threading +import numpy as np +from PIL import Image +from transformers import pipeline, CLIPModel, CLIPProcessor +import torch +from src.types import EngineResult + +logger = logging.getLogger(__name__) +CACHE = os.environ.get("MODEL_CACHE_DIR", "/tmp/models") + +GENERATOR_PROMPTS = { + "real": "a real photograph taken by a camera with natural lighting", + "unknown_gan": "a GAN-generated image with checkerboard artifacts and blurry edges", + "stable_diffusion": "a Stable Diffusion image with painterly soft textures", + "midjourney": "a Midjourney image with cinematic dramatic lighting and hyperdetail", + "dall_e": "a DALL-E image with clean illustration-style and smooth gradients", + "flux": "a FLUX model image with photorealistic precision and sharp detail", + "firefly": "an Adobe Firefly image with commercial stock-photo aesthetics", + "imagen": "a Google Imagen image with precise photorealistic rendering", +} + +_lock = threading.Lock() +_detector = _clip_model = _clip_processor = _backup = None + + +def _load(): + global _detector, _clip_model, _clip_processor, _backup + if _detector is not None: + return + logger.info("Loading fingerprint models...") + _detector = pipeline("image-classification", + model="Organika/sdxl-detector", cache_dir=CACHE) + _clip_model = CLIPModel.from_pretrained( + "openai/clip-vit-large-patch14", cache_dir=CACHE) + _clip_processor = CLIPProcessor.from_pretrained( + "openai/clip-vit-large-patch14", cache_dir=CACHE) + _clip_model.eval() + try: + _backup = pipeline("image-classification", + model="haywoodsloan/ai-image-detector-deploy", + cache_dir=CACHE) + except Exception: + logger.warning("Backup fingerprint detector unavailable") + logger.info("Fingerprint models ready") + + +class FingerprintEngine: + + def _ensure(self): + with _lock: + _load() + + def run(self, image: Image.Image) -> EngineResult: + self._ensure() + if image.mode != "RGB": + image = image.convert("RGB") + + # Binary fake score + FAKE_LABELS = {"artificial", "fake", "ai-generated", "generated"} + try: + preds = _detector(image) + fake_score = max( + (p["score"] for p in preds if p["label"].lower() in FAKE_LABELS), + default=0.5, + ) + except Exception as e: + logger.warning(f"Primary detector error: {e}") + fake_score = 0.5 + + # Ensemble backup + if _backup is not None: + try: + bp = _backup(image) + bk = max((p["score"] for p in bp + if p["label"].lower() in FAKE_LABELS), default=0.5) + fake_score = fake_score * 0.6 + bk * 0.4 + except Exception: + pass + + # CLIP zero-shot generator attribution + generator = "real" + try: + texts = list(GENERATOR_PROMPTS.values()) + inputs = _clip_processor( + text=texts, images=image, + return_tensors="pt", padding=True, truncation=True, + ) + with torch.no_grad(): + logits = _clip_model(**inputs).logits_per_image[0] + probs = logits.softmax(dim=0).numpy() + generator = list(GENERATOR_PROMPTS.keys())[int(np.argmax(probs))] + except Exception as e: + logger.warning(f"CLIP attribution error: {e}") + + if fake_score > 0.65 and generator == "real": + generator = "unknown_gan" + + return EngineResult( + engine="fingerprint", + verdict="FAKE" if fake_score > 0.5 else "REAL", + confidence=float(fake_score), + attributed_generator=generator, + explanation=f"Binary score {fake_score:.2f}; attributed to {generator}.", + ) + + def run_video(self, frames: list) -> EngineResult: + if not frames: + return EngineResult(engine="fingerprint", verdict="UNKNOWN", + confidence=0.5, explanation="No frames.") + keyframes = frames[::8] or [frames[0]] + results = [self.run(Image.fromarray(f)) for f in keyframes] + avg = float(np.mean([r.confidence for r in results])) + gens = [r.attributed_generator for r in results] + top_gen = max(set(gens), key=gens.count) + return EngineResult( + engine="fingerprint", + verdict="FAKE" if avg > 0.5 else "REAL", + confidence=avg, + attributed_generator=top_gen, + explanation=f"Keyframe average {avg:.2f} over {len(keyframes)} frames.", + ) +``` + +--- + +### CoherenceEngine — `src/engines/coherence/engine.py` + +Query `context7.query-docs mediapipe face_mesh` and +`context7.query-docs facenet-pytorch InceptionResnetV1` before modifying. + +```python +import logging, threading, cv2 +import numpy as np +from PIL import Image +from facenet_pytorch import MTCNN, InceptionResnetV1 +import mediapipe as mp +from src.types import EngineResult + +logger = logging.getLogger(__name__) + +_lock = threading.Lock() +_mtcnn = _resnet = _face_mesh = None + + +def _load(): + global _mtcnn, _resnet, _face_mesh + if _mtcnn is not None: + return + logger.info("Loading coherence models...") + _mtcnn = MTCNN(keep_all=False, device="cpu") + _resnet = InceptionResnetV1(pretrained="vggface2").eval() + _face_mesh = mp.solutions.face_mesh.FaceMesh( + static_image_mode=False, max_num_faces=1, + refine_landmarks=True, min_detection_confidence=0.5, + ) + logger.info("Coherence models ready") + + +class CoherenceEngine: + + def _ensure(self): + with _lock: + _load() + + def run(self, image: Image.Image) -> EngineResult: + self._ensure() + frame = np.array(image.convert("RGB")) + score = self._image_score(frame) + return EngineResult( + engine="coherence", + verdict="FAKE" if score > 0.5 else "REAL", + confidence=float(score), + explanation=f"Geometric coherence anomaly {score:.2f} (image mode).", + ) + + def _image_score(self, frame: np.ndarray) -> float: + rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) if frame.shape[2] == 3 else frame + res = _face_mesh.process(rgb) + if not res.multi_face_landmarks: + return 0.35 # no face detected + + lms = res.multi_face_landmarks[0].landmark + h, w = frame.shape[:2] + + def pt(i): + return np.array([lms[i].x * w, lms[i].y * h]) + + # Eye width asymmetry — deepfakes often mismatched + lew = np.linalg.norm(pt(33) - pt(133)) + rew = np.linalg.norm(pt(362) - pt(263)) + eye_ratio = min(lew, rew) / (max(lew, rew) + 1e-9) + eye_score = max(0.0, (0.85 - eye_ratio) / 0.3) + + # Ear symmetry from nose tip + nose = pt(1) + lr = min(np.linalg.norm(nose - pt(234)), np.linalg.norm(nose - pt(454))) + rr = max(np.linalg.norm(nose - pt(234)), np.linalg.norm(nose - pt(454))) + ear_score = max(0.0, (0.90 - lr / (rr + 1e-9)) / 0.2) + + return float(np.clip(eye_score * 0.5 + ear_score * 0.5, 0.0, 1.0)) + + def run_video(self, frames: list[np.ndarray]) -> EngineResult: + self._ensure() + if len(frames) < 4: + r = self.run(Image.fromarray(frames[0])) + r.explanation = "Too few frames for temporal analysis." + return r + + delta = self._embedding_variance(frames) + jerk = self._landmark_jerk(frames) + blink = self._blink_anomaly(frames) + score = float(np.clip(delta * 0.45 + jerk * 0.35 + blink * 0.20, 0.0, 1.0)) + + return EngineResult( + engine="coherence", + verdict="FAKE" if score > 0.5 else "REAL", + confidence=score, + explanation=( + f"Embedding variance {delta:.2f}, " + f"landmark jerk {jerk:.2f}, " + f"blink anomaly {blink:.2f}." + ), + ) + + def _embedding_variance(self, frames: list[np.ndarray]) -> float: + import torch + embeddings = [] + for frame in frames[::4]: + try: + face = _mtcnn(Image.fromarray(frame)) + if face is not None: + with torch.no_grad(): + e = _resnet(face.unsqueeze(0)).numpy()[0] + embeddings.append(e) + except Exception: + continue + if len(embeddings) < 2: + return 0.5 + deltas = [np.linalg.norm(embeddings[i+1] - embeddings[i]) + for i in range(len(embeddings)-1)] + return float(np.clip(np.var(deltas) * 8, 0.0, 1.0)) + + def _landmark_jerk(self, frames: list[np.ndarray]) -> float: + positions = [] + for frame in frames[::2]: + rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + res = _face_mesh.process(rgb) + if res.multi_face_landmarks: + lm = res.multi_face_landmarks[0].landmark + positions.append([lm[1].x, lm[1].y]) + if len(positions) < 4: + return 0.3 + pos = np.array(positions) + jerk = np.diff(pos, n=3, axis=0) + return float(np.clip((np.mean(np.linalg.norm(jerk, axis=1)) - 0.002) / 0.008, + 0.0, 1.0)) + + def _blink_anomaly(self, frames: list[np.ndarray]) -> float: + LEFT_EYE = [33, 160, 158, 133, 153, 144] + RIGHT_EYE = [362, 385, 387, 263, 373, 380] + + def ear(lms, idx, h, w): + pts = [np.array([lms[i].x * w, lms[i].y * h]) for i in idx] + a = np.linalg.norm(pts[1] - pts[5]) + b = np.linalg.norm(pts[2] - pts[4]) + c = np.linalg.norm(pts[0] - pts[3]) + return (a + b) / (2.0 * c + 1e-9) + + ears = [] + for frame in frames: + rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + res = _face_mesh.process(rgb) + if res.multi_face_landmarks: + lm = res.multi_face_landmarks[0].landmark + h, w = frame.shape[:2] + ears.append((ear(lm, LEFT_EYE, h, w) + ear(lm, RIGHT_EYE, h, w)) / 2) + + if len(ears) < 10: + return 0.3 + arr = np.array(ears) + blinks = int(np.sum(np.diff((arr < 0.21).astype(int)) > 0)) + bpm = blinks / (len(ears) / 25) * 60 + if 8 <= bpm <= 25: + return 0.15 + if bpm < 3 or bpm > 35: + return 0.80 + return 0.45 +``` + +--- + +### SSTGNNEngine — `src/engines/sstgnn/engine.py` + +Query `context7.query-docs torch-geometric GCNConv` and +`huggingface model_details dima806/deepfake_vs_real_image_detection` before +modifying. + +```python +import logging, os, threading +import numpy as np +import cv2 +from PIL import Image +from transformers import pipeline +import mediapipe as mp +from scipy.spatial import Delaunay +from src.types import EngineResult + +logger = logging.getLogger(__name__) +CACHE = os.environ.get("MODEL_CACHE_DIR", "/tmp/models") + +_lock = threading.Lock() +_det1 = _det2 = _mesh = None + + +def _load(): + global _det1, _det2, _mesh + if _det1 is not None: + return + logger.info("Loading SSTGNN models...") + _det1 = pipeline("image-classification", + model="dima806/deepfake_vs_real_image_detection", + cache_dir=CACHE) + try: + _det2 = pipeline("image-classification", + model="prithivMLmods/Deep-Fake-Detector-Model", + cache_dir=CACHE) + except Exception: + logger.warning("SSTGNN backup detector unavailable") + _mesh = mp.solutions.face_mesh.FaceMesh( + static_image_mode=True, max_num_faces=1, refine_landmarks=True) + logger.info("SSTGNN models ready") + + +def _fake_prob(preds: list[dict]) -> float: + fake_kw = {"fake", "deepfake", "artificial", "generated", "ai"} + return max( + (p["score"] for p in preds + if any(k in p["label"].lower() for k in fake_kw)), + default=0.5, + ) + + +class SSTGNNEngine: + + def _ensure(self): + with _lock: + _load() + + def run(self, image: Image.Image) -> EngineResult: + self._ensure() + if image.mode != "RGB": + image = image.convert("RGB") + + scores = [] + try: + scores.append(_fake_prob(_det1(image)) * 0.6) + except Exception as e: + logger.warning(f"SSTGNN det1 error: {e}") + if _det2: + try: + scores.append(_fake_prob(_det2(image)) * 0.4) + except Exception as e: + logger.warning(f"SSTGNN det2 error: {e}") + + if not scores: + return EngineResult(engine="sstgnn", verdict="UNKNOWN", + confidence=0.5, explanation="All detectors failed.") + + cnn = sum(scores) / (0.6 if len(scores) == 1 else 1.0) + graph = self._geometry_score(np.array(image)) + final = float(np.clip(cnn * 0.7 + graph * 0.3, 0.0, 1.0)) + + return EngineResult( + engine="sstgnn", + verdict="FAKE" if final > 0.5 else "REAL", + confidence=final, + explanation=f"CNN {cnn:.2f}, geometric graph anomaly {graph:.2f}.", + ) + + def _geometry_score(self, frame: np.ndarray) -> float: + try: + rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + res = _mesh.process(rgb) + if not res.multi_face_landmarks: + return 0.3 + h, w = frame.shape[:2] + lms = res.multi_face_landmarks[0].landmark + idxs = list(range(0, 468, 7))[:68] + pts = np.array([[lms[i].x * w, lms[i].y * h] for i in idxs]) + tri = Delaunay(pts) + areas = [] + for s in tri.simplices: + a, b, c = pts[s] + areas.append(abs(np.cross(b - a, c - a)) / 2) + areas = np.array(areas) + cv_score = float(np.std(areas) / (np.mean(areas) + 1e-9)) + return float(np.clip((cv_score - 0.8) / 1.5, 0.0, 1.0)) + except Exception as e: + logger.warning(f"Geometry score error: {e}") + return 0.3 + + def run_video(self, frames: list[np.ndarray]) -> EngineResult: + self._ensure() + if not frames: + return EngineResult(engine="sstgnn", verdict="UNKNOWN", + confidence=0.5, explanation="No frames.") + sample = frames[::6] or [frames[0]] + results = [self.run(Image.fromarray(f)) for f in sample] + avg = float(np.mean([r.confidence for r in results])) + return EngineResult( + engine="sstgnn", + verdict="FAKE" if avg > 0.5 else "REAL", + confidence=avg, + explanation=f"Frame-sampled SSTGNN average {avg:.2f} over {len(sample)} frames.", + ) +``` + +--- + +## Fusion — `src/fusion/fuser.py` + +```python +import numpy as np +from src.types import EngineResult + +ENGINE_WEIGHTS = { + "fingerprint": 0.45, + "coherence": 0.35, + "sstgnn": 0.20, +} + +ENGINE_WEIGHTS_VIDEO = { + "fingerprint": 0.30, + "coherence": 0.50, + "sstgnn": 0.20, +} + +ATTRIBUTION_PRIORITY = {"fingerprint": 1, "sstgnn": 2, "coherence": 3} + + +def fuse( + results: list[EngineResult], + is_video: bool = False, +) -> tuple[str, float, str]: + """Returns (verdict, confidence, attributed_generator).""" + weights = ENGINE_WEIGHTS_VIDEO if is_video else ENGINE_WEIGHTS + active = [r for r in results if r.verdict != "UNKNOWN"] + + if not active: + return "UNKNOWN", 0.5, "unknown_gan" + + wf = sum(r.confidence * weights.get(r.engine, 0.1) + for r in active if r.verdict == "FAKE") + wr = sum((1 - r.confidence) * weights.get(r.engine, 0.1) + for r in active if r.verdict == "REAL") + + fake_prob = float(np.clip(wf / (wf + wr + 1e-9), 0.0, 1.0)) + verdict = "FAKE" if fake_prob > 0.5 else "REAL" + + generator = "real" + if verdict == "FAKE": + for r in sorted(active, key=lambda r: ATTRIBUTION_PRIORITY.get(r.engine, 9)): + if r.attributed_generator and r.attributed_generator != "real": + generator = r.attributed_generator + break + if generator == "real": + generator = "unknown_gan" + + return verdict, fake_prob, generator +``` + +--- + +## API — `src/api/main.py` + +```python +import asyncio, io, logging, os, time +from pathlib import Path + +import cv2, numpy as np +from fastapi import FastAPI, File, HTTPException, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from PIL import Image + +from src.engines.fingerprint.engine import FingerprintEngine +from src.engines.coherence.engine import CoherenceEngine +from src.engines.sstgnn.engine import SSTGNNEngine +from src.explainability.explainer import explain +from src.fusion.fuser import fuse +from src.services.inference_router import route_inference +from src.types import DetectionResponse + +logger = logging.getLogger(__name__) + +app = FastAPI(title="GenAI-DeepDetect", version="1.0.0") +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], +) + +_fp = FingerprintEngine() +_co = CoherenceEngine() +_st = SSTGNNEngine() + +MAX_MB = int(os.environ.get("MAX_VIDEO_SIZE_MB", 100)) +MAX_FRAMES = int(os.environ.get("MAX_VIDEO_FRAMES", 300)) + +IMAGE_TYPES = {"image/jpeg", "image/png", "image/webp", "image/bmp"} +VIDEO_TYPES = {"video/mp4", "video/quicktime", "video/x-msvideo", "video/webm"} + + +def _extract_frames(path: str) -> list[np.ndarray]: + cap = cv2.VideoCapture(path) + total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + step = max(1, total // MAX_FRAMES) + frames, i = [], 0 + while True: + ret, frame = cap.read() + if not ret: + break + if i % step == 0: + frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + i += 1 + cap.release() + return frames[:MAX_FRAMES] + + +@app.on_event("startup") +async def preload(): + logger.info("Preloading models...") + await asyncio.gather( + asyncio.to_thread(_fp._ensure), + asyncio.to_thread(_co._ensure), + asyncio.to_thread(_st._ensure), + ) + logger.info("All models preloaded") + + +@app.get("/health") +async def health(): + return {"status": "ok"} + + +@app.post("/detect/image", response_model=DetectionResponse) +async def detect_image(file: UploadFile = File(...)): + t0 = time.monotonic() + if file.content_type not in IMAGE_TYPES: + raise HTTPException(400, f"Unsupported type: {file.content_type}") + data = await file.read() + if len(data) > MAX_MB * 1024 * 1024: + raise HTTPException(413, "File too large") + + image = Image.open(io.BytesIO(data)).convert("RGB") + fp, co, st = await asyncio.gather( + asyncio.to_thread(_fp.run, image), + asyncio.to_thread(_co.run, image), + asyncio.to_thread(_st.run, image), + ) + ms = (time.monotonic() - t0) * 1000 + for r in [fp, co, st]: + r.processing_time_ms = ms + + verdict, conf, gen = fuse([fp, co, st], is_video=False) + expl = await asyncio.to_thread(explain, verdict, conf, [fp, co, st], gen) + + return DetectionResponse( + verdict=verdict, confidence=conf, attributed_generator=gen, + explanation=expl, processing_time_ms=ms, + engine_breakdown=[fp, co, st], + ) + + +@app.post("/detect/video", response_model=DetectionResponse) +async def detect_video(file: UploadFile = File(...)): + t0 = time.monotonic() + if file.content_type not in VIDEO_TYPES: + raise HTTPException(400, f"Unsupported type: {file.content_type}") + data = await file.read() + if len(data) > MAX_MB * 1024 * 1024: + raise HTTPException(413, "File too large") + + # Route heavy videos to RunPod + if len(data) > 20 * 1024 * 1024: + return await route_inference(data, "video") + + tmp = Path(f"/tmp/vid_{int(time.time()*1000)}.mp4") + tmp.write_bytes(data) + try: + frames = await asyncio.to_thread(_extract_frames, str(tmp)) + finally: + tmp.unlink(missing_ok=True) + + if not frames: + raise HTTPException(422, "Could not extract frames") + + fp, co, st = await asyncio.gather( + asyncio.to_thread(_fp.run_video, frames), + asyncio.to_thread(_co.run_video, frames), + asyncio.to_thread(_st.run_video, frames), + ) + ms = (time.monotonic() - t0) * 1000 + for r in [fp, co, st]: + r.processing_time_ms = ms + + verdict, conf, gen = fuse([fp, co, st], is_video=True) + expl = await asyncio.to_thread(explain, verdict, conf, [fp, co, st], gen) + + return DetectionResponse( + verdict=verdict, confidence=conf, attributed_generator=gen, + explanation=expl, processing_time_ms=ms, + engine_breakdown=[fp, co, st], + ) +``` + +--- + +## Types — `src/types.py` + +```python +from __future__ import annotations +from typing import Optional +from pydantic import BaseModel + +GENERATOR_LABELS = { + 0: "real", + 1: "unknown_gan", + 2: "stable_diffusion", + 3: "midjourney", + 4: "dall_e", + 5: "flux", + 6: "firefly", + 7: "imagen", +} + + +class EngineResult(BaseModel): + engine: str + verdict: str # FAKE | REAL | UNKNOWN + confidence: float # 0–1 + attributed_generator: Optional[str] = None + explanation: str = "" + processing_time_ms: float = 0.0 + + +class DetectionResponse(BaseModel): + verdict: str + confidence: float + attributed_generator: str + explanation: str + processing_time_ms: float + engine_breakdown: list[EngineResult] +``` + +--- + +## Inference Router — `src/services/inference_router.py` + +```python +import base64, logging, os +import httpx +from src.types import DetectionResponse + +logger = logging.getLogger(__name__) + +RUNPOD_KEY = os.environ.get("RUNPOD_API_KEY", "") +RUNPOD_EID = os.environ.get("RUNPOD_ENDPOINT_ID", "") + + +async def route_inference(data: bytes, media_type: str) -> DetectionResponse: + if not RUNPOD_KEY or not RUNPOD_EID: + raise RuntimeError( + "RunPod not configured. Set RUNPOD_API_KEY and RUNPOD_ENDPOINT_ID." + ) + url = f"https://api.runpod.ai/v2/{RUNPOD_EID}/runsync" + payload = {"input": {"data": base64.b64encode(data).decode(), + "media_type": media_type}} + async with httpx.AsyncClient(timeout=120) as client: + resp = await client.post(url, json=payload, + headers={"Authorization": f"Bearer {RUNPOD_KEY}"}) + resp.raise_for_status() + return DetectionResponse(**resp.json()["output"]) +``` + +--- + +## RunPod Handler — `runpod_handler.py` (project root) + +```python +import base64, io, os, tempfile +import runpod, cv2, numpy as np +from PIL import Image + +os.environ.setdefault("MODEL_CACHE_DIR", "/tmp/models") + +from src.engines.fingerprint.engine import FingerprintEngine +from src.engines.coherence.engine import CoherenceEngine +from src.engines.sstgnn.engine import SSTGNNEngine +from src.explainability.explainer import explain +from src.fusion.fuser import fuse + +_fp = FingerprintEngine() +_co = CoherenceEngine() +_st = SSTGNNEngine() + + +def handler(job: dict) -> dict: + inp = job["input"] + raw = base64.b64decode(inp["data"]) + media_type = inp.get("media_type", "image") + + if media_type == "image": + image = Image.open(io.BytesIO(raw)).convert("RGB") + fp = _fp.run(image) + co = _co.run(image) + st = _st.run(image) + verdict, conf, gen = fuse([fp, co, st], is_video=False) + else: + with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f: + f.write(raw) + tmp = f.name + try: + cap = cv2.VideoCapture(tmp) + frames, i = [], 0 + while True: + ret, frame = cap.read() + if not ret: + break + if i % 4 == 0: + frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + i += 1 + cap.release() + finally: + os.unlink(tmp) + fp = _fp.run_video(frames) + co = _co.run_video(frames) + st = _st.run_video(frames) + verdict, conf, gen = fuse([fp, co, st], is_video=True) + + expl = explain(verdict, conf, [fp, co, st], gen) + + return { + "verdict": verdict, + "confidence": conf, + "attributed_generator": gen, + "explanation": expl, + "processing_time_ms": 0.0, + "engine_breakdown": [r.model_dump() for r in [fp, co, st]], + } + + +runpod.serverless.start({"handler": handler}) +``` + +--- + +## Hosting + +### Option A — HuggingFace Spaces (Free, CPU, primary API host) + +**`spaces/app.py`**: + +```python +import os +os.environ.setdefault("MODEL_CACHE_DIR", "/data/models") +os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") + +import uvicorn +from src.api.main import app + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=7860, workers=1) +``` + +**Root `README.md`** front-matter (Hugging Face reads this file): + +```yaml +--- +title: GenAI DeepDetect +emoji: "🔍" +colorFrom: gray +colorTo: indigo +sdk: docker +app_port: 7860 +pinned: false +--- +``` + +**`Dockerfile`** (replace existing): + +```dockerfile +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y \ + ffmpeg libgl1-mesa-glx libglib2.0-0 libsm6 libxext6 libxrender-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +ENV MODEL_CACHE_DIR=/data/models +ENV TOKENIZERS_PARALLELISM=false +ENV PYTHONUNBUFFERED=1 + +EXPOSE 7860 +CMD ["python", "spaces/app.py"] +``` + +**Secrets to set in HF Spaces** (Settings → Repository secrets): + +``` +GEMINI_API_KEY +HF_TOKEN +RUNPOD_API_KEY +RUNPOD_ENDPOINT_ID +``` + +**Free tier**: 2 vCPU, 16GB RAM, persistent `/data` volume. Models cache to +`/data/models` and survive container restarts. Cold start first request: ~90s. +Warm: <5s. GPU upgrade: T4 at $0.05/hr if needed. + +--- + +### Option B — RunPod Serverless (GPU, heavy video, low cost) + +1. RunPod → Serverless → New Endpoint +2. Select template: `runpod/pytorch:2.1.0-py3.10-cuda11.8.0-devel-ubuntu22.04` +3. Set handler file: `runpod_handler.py` +4. Min replicas: 0, Max: 3 +5. GPU: RTX 3090 or A40 (cheapest that works) +6. Set env vars: `GEMINI_API_KEY`, `HF_TOKEN`, `MODEL_CACHE_DIR=/tmp/models` + +**Cost**: ~$0.0002/request on H100. Billed per second. Min workers = 0 means you +pay nothing when idle — cold start is ~15s. + +**When it triggers**: `inference_router.py` automatically sends videos >20MB to +RunPod. Images always run on HF Spaces. + +--- + +## Frontend — `frontend/lib/api.ts` + +```typescript +const BASE_URL = + process.env.NEXT_PUBLIC_API_URL ?? + 'https://YOUR-USERNAME-genai-deepdetect.hf.space'; + +export type GeneratorLabel = + | 'real' + | 'unknown_gan' + | 'stable_diffusion' + | 'midjourney' + | 'dall_e' + | 'flux' + | 'firefly' + | 'imagen'; + +export interface EngineResult { + engine: string; + verdict: 'FAKE' | 'REAL' | 'UNKNOWN'; + confidence: number; + attributed_generator: GeneratorLabel | null; + explanation: string; + processing_time_ms: number; +} + +export interface DetectionResponse { + verdict: 'FAKE' | 'REAL' | 'UNKNOWN'; + confidence: number; + attributed_generator: GeneratorLabel; + explanation: string; + processing_time_ms: number; + engine_breakdown: EngineResult[]; +} + +async function _post(endpoint: string, file: File): Promise { + const form = new FormData(); + form.append('file', file); + const res = await fetch(`${BASE_URL}${endpoint}`, { + method: 'POST', + body: form, + }); + if (!res.ok) { + const err = await res.text(); + throw new Error(`Detection failed (${res.status}): ${err}`); + } + return res.json(); +} + +export const detectImage = (file: File) => _post('/detect/image', file); +export const detectVideo = (file: File) => _post('/detect/video', file); +``` + +Set in `frontend/.env.local`: + +``` +NEXT_PUBLIC_API_URL=https://your-username-genai-deepdetect.hf.space +``` + +--- + +## Dependencies — `requirements.txt` + +``` +# API +fastapi>=0.111.0 +uvicorn[standard]>=0.29.0 +python-multipart>=0.0.9 +aiofiles>=23.2.1 +httpx>=0.27.0 +pydantic>=2.7.0 + +# ML — fingerprint +transformers>=4.40.0 +timm>=1.0.0 +torch>=2.1.0 +torchvision>=0.16.0 + +# ML — coherence +facenet-pytorch>=2.5.3 +mediapipe>=0.10.14 +opencv-python-headless>=4.9.0 + +# ML — sstgnn +torch-geometric>=2.5.0 +scipy>=1.13.0 + +# Explainability — Gemini +google-generativeai>=0.8.0 + +# HuggingFace +huggingface-hub>=0.23.0 + +# RunPod serverless handler +runpod>=1.6.0 + +# Continual learning +apscheduler>=3.10.4 + +# Utils +Pillow>=10.3.0 +numpy>=1.26.0 +``` + +--- + +## Bug Checklist — Fix Before Running + +### `src/types.py` + +- [ ] `EngineResult` missing `attributed_generator: Optional[str] = None` — add + it +- [ ] `DetectionResponse.engine_breakdown` typed as `list[dict]` — change to + `list[EngineResult]` + +### `src/fusion/fuser.py` + +- [ ] `fuse()` returns 2-tuple — update to return 3-tuple + `(verdict, conf, generator)` +- [ ] Update all callers in `main.py` accordingly + +### `src/explainability/explainer.py` + +- [ ] References `anthropic` SDK — replace entirely with Gemini implementation + above + +### `src/api/main.py` + +- [ ] Missing CORS middleware — add before deploy +- [ ] Missing `@app.on_event("startup")` preload — add it +- [ ] Missing `_extract_frames()` for video — add it +- [ ] `detect_video` likely missing or stubbed — implement fully + +### `src/engines/*/` directories + +- [ ] All three engine files are stubs or empty — replace with full code above + +### `spaces/app.py` + +- [ ] Likely empty — add uvicorn entrypoint + +### `Dockerfile` + +- [ ] Check for `ffmpeg` and `libgl1-mesa-glx` — required for MediaPipe + OpenCV +- [ ] Check `EXPOSE 7860` matches HF Spaces `app_port` + +### `src/services/inference_router.py` + +- [ ] Likely stub — implement `route_inference()` with RunPod httpx call + +--- + +## Code Standards + +- Lazy-load all models behind a threading lock — never load at module import +- Wrap all model inference in `asyncio.to_thread()` — never block the event loop +- Type hints on every function +- `logging.getLogger(__name__)` not `print()` +- `os.environ.get()` not hardcoded secrets +- Pydantic `BaseModel` for all response schemas +- Next.js: pages router only — no `app/` dir, no `src/` dir +- Font: Plus Jakarta Sans or DM Sans — never Inter, Roboto, Arial +- Border radius: 22% icon containers, 18px cards, 12px buttons + +--- + +## MCP Usage Rules + +Every coding session must follow these rules: + +``` +1. Adding a dependency? + → context7: resolve-library-id + → context7: query-docs + +2. Using any HF model? + → huggingface: model_details + → confirm size, license, task, input format + +3. Modifying engine logic? + → context7: query-docs transformers pipeline (fingerprint) + → context7: query-docs mediapipe face_mesh (coherence) + → context7: query-docs torch-geometric GCNConv (sstgnn) + → context7: query-docs facenet-pytorch (coherence embeddings) + +4. Modifying Gemini calls? + → context7: query-docs google-generativeai GenerativeModel + +5. Modifying RunPod handler? + → context7: query-docs runpod serverless handler + +6. Modifying FastAPI routes? + → context7: query-docs fastapi UploadFile + +7. Frontend API changes? + → context7: query-docs next.js pages-router fetch +``` + +--- + +## Friday Deploy Checklist + +``` +[ ] pip install -r requirements.txt (no errors) +[ ] src/types.py — EngineResult has attributed_generator +[ ] src/types.py — DetectionResponse has engine_breakdown: list[EngineResult] +[ ] src/fusion/fuser.py — returns 3-tuple +[ ] src/explainability/explainer.py — uses Gemini, no anthropic import +[ ] src/engines/fingerprint/engine.py — full implementation +[ ] src/engines/coherence/engine.py — full implementation +[ ] src/engines/sstgnn/engine.py — full implementation +[ ] src/api/main.py — CORS + startup preload + video route +[ ] src/services/inference_router.py — RunPod httpx call +[ ] runpod_handler.py — added to project root +[ ] spaces/app.py — uvicorn entrypoint +[ ] Dockerfile — has ffmpeg, libgl1, EXPOSE 7860 +[ ] HF Space created + secrets set + pushed +[ ] RunPod endpoint deployed + endpoint ID noted +[ ] frontend/.env.local — NEXT_PUBLIC_API_URL points to HF Space +[ ] Vercel deploy of frontend/ + +Smoke tests: +[ ] GET /health → {"status":"ok"} +[ ] POST /detect/image (real JPEG) → verdict REAL +[ ] POST /detect/image (AI PNG) → verdict FAKE +[ ] POST /detect/video (MP4 <20MB) → response within 30s +[ ] POST /detect/video (MP4 >20MB) → routes to RunPod +``` + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..535945b48bafa5b74c277e8f24153bc708b6d8d4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y \ + ffmpeg libgl1-mesa-glx libglib2.0-0 libsm6 libxext6 libxrender-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +ENV MODEL_CACHE_DIR=/data/models +ENV TOKENIZERS_PARALLELISM=false +ENV PYTHONUNBUFFERED=1 + +EXPOSE 7860 +CMD ["python", "spaces/app.py"] diff --git a/README.md b/README.md index 999bc2e64fddc5f954a3882439b8203fe0784377..d54a72ceb4f8c71122d6cf7b0946320f54a14c4a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,19 @@ --- -title: Deepdetection -emoji: 📈 -colorFrom: indigo -colorTo: purple +title: GenAI DeepDetect +emoji: "??" +colorFrom: gray +colorTo: indigo sdk: docker +app_port: 7860 pinned: false -license: mit -short_description: A --- -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +# GenAI-DeepDetect + +Docker-based Hugging Face Space for multimodal deepfake detection. + +This Space runs the FastAPI service from `src/api/main.py` and exposes: + +- `GET /health` +- `POST /detect/image` +- `POST /detect/video` diff --git a/configs/coherence_baseline.yaml b/configs/coherence_baseline.yaml new file mode 100644 index 0000000000000000000000000000000000000000..066e9cfe985d949bb34bf42dc3f7364534e14706 --- /dev/null +++ b/configs/coherence_baseline.yaml @@ -0,0 +1,44 @@ +# configs/coherence_baseline.yaml +# Training config for the Coherence (lip-audio sync) engine — Phase 2 + +engine: coherence + +kaggle_datasets: + - xhlulu/faceforensics-in-compressed-videos + - reubensinclair/celeb-df + +kaggle_competitions: + - deepfake-detection-challenge # DFDC — requires competition acceptance + +data: + processed_dir: /kaggle/working/processed/coherence + sources: + - mount: /kaggle/input/faceforensics-in-compressed-videos + - mount: /kaggle/input/deepfake-detection-challenge + - mount: /kaggle/input/celeb-df + +preprocess_args: + workers: 4 + max_clips: 20 + +model: + visual_stream: resnet18_truncated_layer3 + audio_stream: 1d_cnn + embed_dim: 256 + dropout: 0.3 + +train_args: + data_dir: /kaggle/working/processed/coherence + output_dir: /kaggle/working/checkpoints/coherence + epochs: 25 + batch_size: 16 + lr: 1.0e-4 + weight_decay: 1.0e-4 + contrastive_weight: 0.1 + amp: true + seed: 42 + patience: 5 + +targets: + val_auc: 0.88 + eer: 0.15 diff --git a/configs/fingerprint_baseline.yaml b/configs/fingerprint_baseline.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ab716815d427564265d93dba25009a6cadf43377 --- /dev/null +++ b/configs/fingerprint_baseline.yaml @@ -0,0 +1,46 @@ +# configs/fingerprint_baseline.yaml +# Training config for the Fingerprint (ViT) engine — Phase 1 + +engine: fingerprint + +kaggle_datasets: + - xhlulu/140k-real-and-fake-faces + - philosopher0/ai-generated-vs-real-images-datasaet + +kaggle_competitions: [] + +data: + processed_dir: /kaggle/working/processed/fingerprint + sources: + - slug: xhlulu/140k-real-and-fake-faces + mount: /kaggle/input/140k-real-and-fake-faces + - slug: philosopher0/ai-generated-vs-real-images-datasaet + mount: /kaggle/input/ai-generated-vs-real-images-datasaet + +model: + name: vit_base_patch16_224 + pretrained: true + num_binary_classes: 2 + num_generator_classes: 8 # must match GENERATOR_CLASSES in src/training/config.py + +train_args: + data_dir: /kaggle/working/processed/fingerprint + output_dir: /kaggle/working/checkpoints/fingerprint + epochs: 30 + batch_size: 64 + lr: 2.0e-5 + weight_decay: 0.01 + warmup_steps: 500 + grad_accum: 2 + amp: true + seed: 42 + patience: 5 + +demo_args: + epochs: 5 + batch_size: 64 + +targets: + val_auc: 0.92 + test_auc: 0.90 + cross_dataset_auc: 0.80 diff --git a/configs/sstgnn_baseline.yaml b/configs/sstgnn_baseline.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a9a38ad73d70b27e3b9890bfdc5c00a753defff6 --- /dev/null +++ b/configs/sstgnn_baseline.yaml @@ -0,0 +1,49 @@ +# configs/sstgnn_baseline.yaml +# Training config for the SSTGNN (graph attention) engine — Phase 3 +# +# IMPORTANT: use T4x2 accelerator, NOT P100. +# Do NOT set amp: true — mixed precision causes NaN in GAT layers. + +engine: sstgnn + +kaggle_datasets: + - xhlulu/faceforensics-in-compressed-videos + - reubensinclair/celeb-df + +kaggle_competitions: + - deepfake-detection-challenge + +data: + processed_dir: /kaggle/working/processed/sstgnn + +landmark_args: + window_frames: 64 + window_stride: 32 + num_landmarks: 68 + min_detection_confidence: 0.3 # FIX: was 0.5 — too strict for DFDC low-res + min_tracking_confidence: 0.3 + min_video_height: 240 + +model: + in_channels: 5 + hidden_dim: 64 + heads: 4 + num_gat_layers: 3 + dropout: 0.3 + out_dropout: 0.4 + +train_args: + data_dir: /kaggle/working/processed/sstgnn + output_dir: /kaggle/working/checkpoints/sstgnn + epochs: 40 + batch_size: 8 + lr: 5.0e-4 + weight_decay: 5.0e-4 + scheduler: cosine + seed: 42 + patience: 8 + amp: false # NEVER set true for SSTGNN + +targets: + val_auc: 0.85 + test_auc: 0.83 diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a982bae08467197997d560b0e5e92fd453bb0448 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,25 @@ +# Stage 1 — install deps +FROM node:20-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci + +# Stage 2 — build +FROM node:20-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npm run build + +# Stage 3 — runtime +FROM node:20-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production + +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/public ./public + +EXPOSE 3000 +CMD ["npm", "start"] diff --git a/frontend/components/ui/background-paths.tsx b/frontend/components/ui/background-paths.tsx new file mode 100644 index 0000000000000000000000000000000000000000..88f9acd60d0a3154f4439fcdc801181f382619a3 --- /dev/null +++ b/frontend/components/ui/background-paths.tsx @@ -0,0 +1,50 @@ +'use client' + +interface Props { + opacity?: number +} + +export default function BackgroundPaths({ opacity = 0.04 }: Props) { + return ( + + ) +} diff --git a/frontend/components/ui/confidence-ring.tsx b/frontend/components/ui/confidence-ring.tsx new file mode 100644 index 0000000000000000000000000000000000000000..73e4c292856aff4a240d5bf5f774230a7df7c6d6 --- /dev/null +++ b/frontend/components/ui/confidence-ring.tsx @@ -0,0 +1,67 @@ +import { motion } from 'framer-motion' +import type { Verdict } from '@/types/detection' +import { formatConfidence } from '@/lib/utils' + +interface Props { + confidence: number + verdict: Verdict +} + +const RADIUS = 36 +const CIRCUMFERENCE = 2 * Math.PI * RADIUS + +export default function ConfidenceRing({ confidence, verdict }: Props) { + const dashOffset = CIRCUMFERENCE * (1 - confidence) + const color = verdict === 'FAKE' ? '#ef4444' : '#22c55e' + const bgColor = verdict === 'FAKE' ? 'rgba(239,68,68,0.1)' : 'rgba(34,197,94,0.08)' + + return ( +
+ {/* SVG ring */} +
+ + {/* Track */} + + {/* Progress */} + + +
+ + {Math.round(confidence * 100)}% + +
+
+ +
+
+ Confidence +
+
+ {formatConfidence(confidence)} +
+
+ overall {verdict.toLowerCase()} confidence +
+
+
+ ) +} diff --git a/frontend/components/ui/engine-breakdown.tsx b/frontend/components/ui/engine-breakdown.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2d7927b1867c732ba6baad71d84397e09d29c12c --- /dev/null +++ b/frontend/components/ui/engine-breakdown.tsx @@ -0,0 +1,66 @@ +import { motion } from 'framer-motion' +import type { EngineResult } from '@/types/detection' +import VerdictBadge from './verdict-badge' +import { ENGINE_LABELS } from '@/lib/constants' +import { formatMs } from '@/lib/utils' + +interface Props { + engines: EngineResult[] +} + +export default function EngineBreakdown({ engines }: Props) { + return ( +
+ {engines.map((engine, i) => { + const isNA = engine.explanation.startsWith('N/A') + return ( + + {/* Engine name */} + + {ENGINE_LABELS[engine.engine] ?? engine.engine.toUpperCase()} + + + {/* Verdict badge or NA */} + {isNA ? ( + + VIDEO ONLY + + ) : ( + + )} + + {/* Confidence bar */} +
+ {!isNA && ( +
+ +
+ )} +
+ + {/* Score */} + + {isNA ? '—' : `${Math.round(engine.confidence * 100)}%`} + + + {/* Time */} + + {engine.processing_time_ms > 0 ? formatMs(engine.processing_time_ms) : '—'} + +
+ ) + })} +
+ ) +} diff --git a/frontend/components/ui/liquid-glass-button.tsx b/frontend/components/ui/liquid-glass-button.tsx new file mode 100644 index 0000000000000000000000000000000000000000..805c052d8ec594264225e52a52fe3a02c1262b82 --- /dev/null +++ b/frontend/components/ui/liquid-glass-button.tsx @@ -0,0 +1,31 @@ +import { cn } from '@/lib/utils' +import type { ButtonHTMLAttributes } from 'react' + +interface Props extends ButtonHTMLAttributes { + variant?: 'primary' | 'ghost' + size?: 'sm' | 'md' +} + +export default function LiquidGlassButton({ + children, + variant = 'primary', + size = 'md', + className, + ...rest +}: Props) { + return ( + + ) +} diff --git a/frontend/components/ui/loading-orbit.tsx b/frontend/components/ui/loading-orbit.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fe0691cb23eb2d8cabdcf2e7f029e9e2eb05113a --- /dev/null +++ b/frontend/components/ui/loading-orbit.tsx @@ -0,0 +1,67 @@ +import { useState, useEffect } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { LOADING_STAGES } from '@/lib/constants' + +export default function LoadingOrbit() { + const [stageIndex, setStageIndex] = useState(0) + + useEffect(() => { + const id = setInterval(() => { + setStageIndex(i => (i + 1) % LOADING_STAGES.length) + }, 2800) + return () => clearInterval(id) + }, []) + + return ( +
+ {/* Spinning ring */} +
+ {/* Static inner circle */} +
+ {/* Outer orbit ring */} + + {/* Center label */} + + SCANNING + +
+ + {/* Stage label — animates on change */} + + + {LOADING_STAGES[stageIndex]} + + + + {/* Dots */} +
+ {LOADING_STAGES.map((_, i) => ( +
+ ))} +
+
+ ) +} diff --git a/frontend/components/ui/media-meta-strip.tsx b/frontend/components/ui/media-meta-strip.tsx new file mode 100644 index 0000000000000000000000000000000000000000..092275230e82a8d015f19eddd0961383ea62b483 --- /dev/null +++ b/frontend/components/ui/media-meta-strip.tsx @@ -0,0 +1,23 @@ +import { formatBytes } from '@/lib/utils' + +interface Props { + file: File +} + +export default function MediaMetaStrip({ file }: Props) { + const ext = file.name.split('.').pop()?.toUpperCase() ?? '—' + + return ( +
+ + File + +
+

{file.name}

+

+ {ext} · {formatBytes(file.size)} +

+
+
+ ) +} diff --git a/frontend/components/ui/radial-orbital-timeline.tsx b/frontend/components/ui/radial-orbital-timeline.tsx new file mode 100644 index 0000000000000000000000000000000000000000..41c425e73cad65bde9469a552cee40a2559ec320 --- /dev/null +++ b/frontend/components/ui/radial-orbital-timeline.tsx @@ -0,0 +1,73 @@ +import type { LucideIcon } from 'lucide-react' + +export interface OrbitalNode { + label: string + Icon: LucideIcon +} + +interface Props { + nodes: OrbitalNode[] + centerLabel?: string + radius?: number + size?: number +} + +function nodePosition(index: number, total: number, radius: number) { + const angle = (Math.PI / 2) - (index * 2 * Math.PI) / total + return { + x: Math.round(radius * Math.cos(angle)), + y: Math.round(-radius * Math.sin(angle)), + } +} + +export default function RadialOrbitalTimeline({ + nodes, + centerLabel = 'DETECT', + radius = 140, + size = 320, +}: Props) { + return ( +
+ {/* Orbit rings */} + {[size * 0.875, size * 0.625].map((d, i) => ( +
+ ))} + + {/* Center */} +
+ + GENAI
{centerLabel} +
+
+ + {/* Nodes */} + {nodes.map((node, i) => { + const pos = nodePosition(i, nodes.length, radius) + return ( +
+
+ +
+ + {node.label} + +
+ ) + })} +
+ ) +} diff --git a/frontend/components/ui/upload-zone.tsx b/frontend/components/ui/upload-zone.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4cb6867ee3630468f3ddcfb7ae0503cec7090b45 --- /dev/null +++ b/frontend/components/ui/upload-zone.tsx @@ -0,0 +1,96 @@ +import { useRef, useState, useCallback } from 'react' +import { Image as ImageIcon, Film } from 'lucide-react' +import { cn } from '@/lib/utils' +import { + ALLOWED_IMAGE_TYPES, ALLOWED_VIDEO_TYPES, + IMAGE_FORMAT_LABEL, VIDEO_FORMAT_LABEL, + MAX_IMAGE_MB, MAX_VIDEO_MB, +} from '@/lib/constants' + +interface Props { + mediaType: 'image' | 'video' + onFile: (file: File) => void +} + +export default function UploadZone({ mediaType, onFile }: Props) { + const inputRef = useRef(null) + const [dragging, setDragging] = useState(false) + const [error, setError] = useState(null) + + const accept = mediaType === 'image' ? ALLOWED_IMAGE_TYPES.join(',') : ALLOWED_VIDEO_TYPES.join(',') + const formatLabel = mediaType === 'image' ? IMAGE_FORMAT_LABEL : VIDEO_FORMAT_LABEL + const maxMb = mediaType === 'image' ? MAX_IMAGE_MB : MAX_VIDEO_MB + const Icon = mediaType === 'image' ? ImageIcon : Film + + const validate = useCallback((file: File): string | null => { + const allowed = mediaType === 'image' ? ALLOWED_IMAGE_TYPES : ALLOWED_VIDEO_TYPES + if (file.type && !allowed.includes(file.type)) { + return `Unsupported format. Allowed: ${formatLabel}` + } + if (file.size > maxMb * 1024 * 1024) { + return `File too large. Max ${maxMb}MB.` + } + return null + }, [mediaType, formatLabel, maxMb]) + + const handleFiles = useCallback((files: FileList | null) => { + if (!files || files.length === 0) return + const file = files[0] + const err = validate(file) + if (err) { setError(err); return } + setError(null) + onFile(file) + }, [validate, onFile]) + + const onDrop = (e: React.DragEvent) => { + e.preventDefault() + setDragging(false) + handleFiles(e.dataTransfer.files) + } + + return ( +
+ + + {error && ( +

+ {error} +

+ )} +
+ ) +} diff --git a/frontend/components/ui/verdict-badge.tsx b/frontend/components/ui/verdict-badge.tsx new file mode 100644 index 0000000000000000000000000000000000000000..87ca805b58426ebf00d64c222e6f878ac2339d6e --- /dev/null +++ b/frontend/components/ui/verdict-badge.tsx @@ -0,0 +1,23 @@ +import type { Verdict } from '@/types/detection' +import { cn } from '@/lib/utils' + +interface Props { + verdict: Verdict + size?: 'sm' | 'md' +} + +export default function VerdictBadge({ verdict, size = 'sm' }: Props) { + return ( + + {verdict} + + ) +} diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts new file mode 100644 index 0000000000000000000000000000000000000000..f5d3f8164eb17170ca9ecf6ccc4bc12412643072 --- /dev/null +++ b/frontend/lib/api.ts @@ -0,0 +1,33 @@ +// frontend/lib/api.ts + +import type { DetectionResponse } from '@/types/detection' + +const BASE_URL = + process.env.NEXT_PUBLIC_API_URL ?? + 'http://localhost:8000' + +async function _post(endpoint: string, file: File): Promise { + const form = new FormData() + form.append('file', file) + + const res = await fetch(`${BASE_URL}${endpoint}`, { + method: 'POST', + body: form, + }) + + if (!res.ok) { + const err = await res.text() + throw new Error(`Detection failed (${res.status}): ${err}`) + } + + return res.json() +} + +export const detectImage = (file: File) => _post('/detect/image', file) +export const detectVideo = (file: File) => _post('/detect/video', file) + +export async function healthCheck(): Promise<{ status: string; version?: string }> { + const res = await fetch(`${BASE_URL}/health`) + if (!res.ok) throw new Error(`Health check failed: ${res.status}`) + return res.json() +} diff --git a/frontend/lib/constants.ts b/frontend/lib/constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..027cdddcd4a964f253b6c21bf9862826af5f3847 --- /dev/null +++ b/frontend/lib/constants.ts @@ -0,0 +1,46 @@ +// frontend/lib/constants.ts + +import type { GeneratorLabel } from '@/types/detection' + +export const MAX_IMAGE_MB = 20 +export const MAX_VIDEO_MB = 500 + +export const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp'] +export const ALLOWED_VIDEO_TYPES = ['video/mp4', 'video/quicktime', 'video/x-msvideo'] + +export const IMAGE_FORMAT_LABEL = 'JPG - PNG - WEBP - MAX 20MB' +export const VIDEO_FORMAT_LABEL = 'MP4 - MOV - AVI - MAX 500MB' + +export const GENERATOR_LABELS: Record = { + real: 'Real', + unknown_gan: 'Unknown GAN', + stable_diffusion: 'Stable Diffusion', + midjourney: 'Midjourney', + dall_e: 'DALL-E', + flux: 'FLUX', + firefly: 'Adobe Firefly', + imagen: 'Google Imagen', + sora: 'OpenAI Sora', +} + +export const ENGINE_LABELS: Record = { + fingerprint: 'FINGERPRINT', + coherence: 'COHERENCE', + sstgnn: 'SSTGNN', +} + +export const ORBITAL_NODES = [ + { label: 'Image', icon: 'Image' }, + { label: 'Video', icon: 'Film' }, + { label: 'Audio', icon: 'Mic' }, + { label: 'Face Mesh', icon: 'Scan' }, + { label: 'Generator ID', icon: 'Cpu' }, + { label: 'Confidence', icon: 'BarChart2' }, + { label: 'Report', icon: 'FileText' }, +] as const + +export const LOADING_STAGES = [ + 'FINGERPRINT SCAN', + 'COHERENCE CHECK', + 'GRAPH ANALYSIS', +] as const diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..a1a8e6aada2d0227a79058349882a8bc2ec7eb31 --- /dev/null +++ b/frontend/lib/utils.ts @@ -0,0 +1,29 @@ +// frontend/lib/utils.ts + +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} + +export function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}` +} + +export function formatMs(ms: number): string { + if (ms < 1000) return `${Math.round(ms)}ms` + return `${(ms / 1000).toFixed(1)}s` +} + +export function formatConfidence(confidence: number): string { + return `${(confidence * 100).toFixed(1)}%` +} + +export function clampConfidence(v: number): number { + return Math.max(0, Math.min(1, v)) +} diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..254b73c165d9020f2076dc5a74c24bc346e26a2b --- /dev/null +++ b/frontend/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/frontend/next.config.js b/frontend/next.config.js new file mode 100644 index 0000000000000000000000000000000000000000..ce18584e5048b7a6dd8d5d5c99ade7529b3cf0a1 --- /dev/null +++ b/frontend/next.config.js @@ -0,0 +1,17 @@ +const path = require('path') + +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + outputFileTracingRoot: path.join(__dirname), + async rewrites() { + return [ + { + source: '/api/:path*', + destination: `${process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8000'}/:path*`, + }, + ] + }, +} + +module.exports = nextConfig diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..fe187b7506c0fb0670cd7fa410632440c56ab2ea --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,14873 @@ +{ + "name": "genai-deepdetect-ui", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "genai-deepdetect-ui", + "version": "0.1.0", + "dependencies": { + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "framer-motion": "^12.38.0", + "lucide-react": "^0.323.0", + "next": "^15.5.14", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwind-merge": "^2.2.1", + "update": "^0.7.4" + }, + "devDependencies": { + "@types/node": "^20.11.5", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "autoprefixer": "^10.4.17", + "eslint": "^8.56.0", + "eslint-config-next": "^15.5.14", + "postcss": "^8.4.33", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.14.tgz", + "integrity": "sha512-aXeirLYuASxEgi4X4WhfXsShCFxWDfNn/8ZeC5YXAS2BB4A8FJi1kwwGL6nvMVboE7fZCzmJPNdMvVHc8JpaiA==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.14.tgz", + "integrity": "sha512-ogBjgsFrPPz19abP3VwcYSahbkUOMMvJjxCOYWYndw+PydeMuLuB4XrvNkNutFrTjC9St2KFULRdKID8Sd/CMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@next/eslint-plugin-next/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.14.tgz", + "integrity": "sha512-Y9K6SPzobnZvrRDPO2s0grgzC+Egf0CqfbdvYmQVaztV890zicw8Z8+4Vqw8oPck8r1TjUHxVh8299Cg4TrxXg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.14.tgz", + "integrity": "sha512-aNnkSMjSFRTOmkd7qoNI2/rETQm/vKD6c/Ac9BZGa9CtoOzy3c2njgz7LvebQJ8iPxdeTuGnAjagyis8a9ifBw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.14.tgz", + "integrity": "sha512-tjlpia+yStPRS//6sdmlVwuO1Rioern4u2onafa5n+h2hCS9MAvMXqpVbSrjgiEOoCs0nJy7oPOmWgtRRNSM5Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.14.tgz", + "integrity": "sha512-8B8cngBaLadl5lbDRdxGCP1Lef8ipD6KlxS3v0ElDAGil6lafrAM3B258p1KJOglInCVFUjk751IXMr2ixeQOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.14.tgz", + "integrity": "sha512-bAS6tIAg8u4Gn3Nz7fCPpSoKAexEt2d5vn1mzokcqdqyov6ZJ6gu6GdF9l8ORFrBuRHgv3go/RfzYz5BkZ6YSQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.14.tgz", + "integrity": "sha512-mMxv/FcrT7Gfaq4tsR22l17oKWXZmH/lVqcvjX0kfp5I0lKodHYLICKPoX1KRnnE+ci6oIUdriUhuA3rBCDiSw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.14.tgz", + "integrity": "sha512-OTmiBlYThppnvnsqx0rBqjDRemlmIeZ8/o4zI7veaXoeO1PVHoyj2lfTfXTiiGjCyRDhA10y4h6ZvZvBiynr2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.14.tgz", + "integrity": "sha512-+W7eFf3RS7m4G6tppVTOSyP9Y6FsJXfOuKzav1qKniiFm3KFByQfPEcouHdjlZmysl4zJGuGLQ/M9XyVeyeNEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.16.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.37", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.2", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.16.0", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-bgblack": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgblack/-/ansi-bgblack-0.1.1.tgz", + "integrity": "sha512-tp8M/NCmSr6/skdteeo9UgJ2G1rG88X3ZVNZWXUxFw4Wh0PAGaAAWQS61sfBt/1QNcwMTY3EBKOMPujwioJLaw==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-bgblue": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgblue/-/ansi-bgblue-0.1.1.tgz", + "integrity": "sha512-R8JmX2Xv3+ichUQE99oL+LvjsyK+CDWo/BtVb4QUz3hOfmf2bdEmiDot3fQcpn2WAHW3toSRdjSLm6bgtWRDlA==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-bgcyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgcyan/-/ansi-bgcyan-0.1.1.tgz", + "integrity": "sha512-6SByK9q2H978bmqzuzA5NPT1lRDXl3ODLz/DjC4URO5f/HqK7dnRKfoO/xQLx/makOz7zWIbRf6+Uf7bmaPSkQ==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-bggreen": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bggreen/-/ansi-bggreen-0.1.1.tgz", + "integrity": "sha512-8TRtOKmIPOuxjpklrkhUbqD2NnVb4WZQuIjXrT+TGKFKzl7NrL7wuNvEap3leMt2kQaCngIN1ZzazSbJNzF+Aw==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-bgmagenta": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgmagenta/-/ansi-bgmagenta-0.1.1.tgz", + "integrity": "sha512-UZYhobiGAlV4NiwOlKAKbkCyxOl1PPZNvdIdl/Ce5by45vwiyNdBetwHk/AjIpo1Ji9z+eE29PUBAjjfVmz5SA==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-bgred": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgred/-/ansi-bgred-0.1.1.tgz", + "integrity": "sha512-BpPHMnYmRBhcjY5knRWKjQmPDPvYU7wrgBSW34xj7JCH9+a/SEIV7+oSYVOgMFopRIadOz9Qm4zIy+mEBvUOPA==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-bgwhite": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgwhite/-/ansi-bgwhite-0.1.1.tgz", + "integrity": "sha512-KIF19t+HOYOorUnHTOhZpeZ3bJsjzStBG2hSGM0WZ8YQQe4c7lj9CtwnucscJDPrNwfdz6GBF+pFkVfvHBq6uw==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-bgyellow": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bgyellow/-/ansi-bgyellow-0.1.1.tgz", + "integrity": "sha512-WyRoOFSIvOeM7e7YdlSjfAV82Z6K1+VUVbygIQ7C/VGzWYuO/d30F0PG7oXeo4uSvSywR0ozixDQvtXJEorq4Q==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-black": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-black/-/ansi-black-0.1.1.tgz", + "integrity": "sha512-hl7re02lWus7lFOUG6zexhoF5gssAfG5whyr/fOWK9hxNjUFLTjhbU/b4UHWOh2dbJu9/STSUv+80uWYzYkbTQ==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-blue": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-blue/-/ansi-blue-0.1.1.tgz", + "integrity": "sha512-8Um59dYNDdQyoczlf49RgWLzYgC2H/28W3JAIyOAU/+WkMcfZmaznm+0i1ikrE0jME6Ypk9CJ9CY2+vxbPs7Fg==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-bold": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-bold/-/ansi-bold-0.1.1.tgz", + "integrity": "sha512-wWKwcViX1E28U6FohtWOP4sHFyArELHJ2p7+3BzbibqJiuISeskq6t7JnrLisUngMF5zMhgmXVw8Equjzz9OlA==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-colors": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-0.2.0.tgz", + "integrity": "sha512-ScRNUT0TovnYw6+Xo3iKh6G+VXDw2Ds7ZRnMIuKBgHY02DgvT2T2K22/tc/916Fi0W/5Z1RzDaHQwnp75hqdbA==", + "license": "MIT", + "dependencies": { + "ansi-bgblack": "^0.1.1", + "ansi-bgblue": "^0.1.1", + "ansi-bgcyan": "^0.1.1", + "ansi-bggreen": "^0.1.1", + "ansi-bgmagenta": "^0.1.1", + "ansi-bgred": "^0.1.1", + "ansi-bgwhite": "^0.1.1", + "ansi-bgyellow": "^0.1.1", + "ansi-black": "^0.1.1", + "ansi-blue": "^0.1.1", + "ansi-bold": "^0.1.1", + "ansi-cyan": "^0.1.1", + "ansi-dim": "^0.1.1", + "ansi-gray": "^0.1.1", + "ansi-green": "^0.1.1", + "ansi-grey": "^0.1.1", + "ansi-hidden": "^0.1.1", + "ansi-inverse": "^0.1.1", + "ansi-italic": "^0.1.1", + "ansi-magenta": "^0.1.1", + "ansi-red": "^0.1.1", + "ansi-reset": "^0.1.1", + "ansi-strikethrough": "^0.1.1", + "ansi-underline": "^0.1.1", + "ansi-white": "^0.1.1", + "ansi-yellow": "^0.1.1", + "lazy-cache": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-cyan": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", + "integrity": "sha512-eCjan3AVo/SxZ0/MyIYRtkpxIu/H3xZN7URr1vXVrISxeyz8fUFz0FJziamK4sS8I+t35y4rHg1b2PklyBe/7A==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-dim": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-dim/-/ansi-dim-0.1.1.tgz", + "integrity": "sha512-zAfb1fokXsq4BoZBkL0eK+6MfFctbzX3R4UMcoWrL1n2WHewFKentTvOZv2P11u6P4NtW/V47hVjaN7fJiefOg==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-gray": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", + "integrity": "sha512-HrgGIZUl8h2EHuZaU9hTR/cU5nhKxpVE1V6kdGsQ8e4zirElJ5fvtfc8N7Q1oq1aatO275i8pUFUCpNWCAnVWw==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-green": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-green/-/ansi-green-0.1.1.tgz", + "integrity": "sha512-WJ70OI4jCaMy52vGa/ypFSKFb/TrYNPaQ2xco5nUwE0C5H8piume/uAZNNdXXiMQ6DbRmiE7l8oNBHu05ZKkrw==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-grey": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-grey/-/ansi-grey-0.1.1.tgz", + "integrity": "sha512-+J1nM4lC+whSvf3T4jsp1KR+C63lypb+VkkwtLQMc1Dlt+nOvdZpFT0wwFTYoSlSwCcLUAaOpHF6kPkYpSa24A==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-hidden": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-hidden/-/ansi-hidden-0.1.1.tgz", + "integrity": "sha512-8gB1bo9ym9qZ/Obvrse1flRsfp2RE+40B23DhQcKxY+GSeaOJblLnzBOxzvmLTWbi5jNON3as7wd9rC0fNK73Q==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-inverse": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-inverse/-/ansi-inverse-0.1.1.tgz", + "integrity": "sha512-Kq8Z0dBRhQhDMN/Rso1Nu9niwiTsRkJncfJZXiyj7ApbfJrGrrubHXqXI37feJZkYcIx6SlTBdNCeK0OQ6X6ag==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-italic": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-italic/-/ansi-italic-0.1.1.tgz", + "integrity": "sha512-jreCxifSAqbaBvcibeQxcwhQDbEj7gF69XnpA6x83qbECEBaRBD1epqskrmov1z4B+zzQuEdwbWxgzvhKa+PkA==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-magenta": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-magenta/-/ansi-magenta-0.1.1.tgz", + "integrity": "sha512-A1Giu+HRwyWuiXKyXPw2AhG1yWZjNHWO+5mpt+P+VWYkmGRpLPry0O5gmlJQEvpjNpl4RjFV7DJQ4iozWOmkbQ==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-reset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-reset/-/ansi-reset-0.1.1.tgz", + "integrity": "sha512-n+D0qD3B+h/lP0dSwXX1SZMoXufdUVotLMwUuvXa50LtBAh3f+WV8b5nFMfLL/hgoPBUt+rG/pqqzF8krlZKcw==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-strikethrough": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-strikethrough/-/ansi-strikethrough-0.1.1.tgz", + "integrity": "sha512-gWkLPDvHH2pC9YEKqp8dIl0mg3sRglMPvioqGDIOXiwxjxUwIJ1gF86E2o4R5yLNh8IAkwHbaMtASkJfkQ2hIA==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-underline": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-underline/-/ansi-underline-0.1.1.tgz", + "integrity": "sha512-D+Bzwio/0/a0Fu5vJzrIT6bFk43TW46vXfSvzysOTEHcXOAUJTVMHWDbELIzGU4AVxVw2rCTb7YyWS4my2cSKQ==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-white": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-white/-/ansi-white-0.1.1.tgz", + "integrity": "sha512-DJHaF2SRzBb9wZBgqIJNjjTa7JUJTO98sHeTS1sDopyKKRopL1KpaJ20R6W2f/ZGras8bYyIZDtNwYOVXNgNFg==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-yellow": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-yellow/-/ansi-yellow-0.1.1.tgz", + "integrity": "sha512-6E3D4BQLXHLl3c/NwirWVZ+BCkMq2qsYxdeAGGOijKrx09FaqU+HktFL6QwAwNvgJiMLnv6AQ2C1gFZx0h1CBg==", + "license": "MIT", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha512-dtXTVMkh6VkEEA7OhXnN1Ecb8aAGFdZ1LFxtOCoqj4qkyOJMt7+qs6Ahdy6p/NQCPYsRSXXivhSB/J5E9jmYKA==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", + "integrity": "sha512-tVqVTHt+Q5Xb09qRkbu+DidW1yYzz5izWS2Xm2yFm7qJnmUfz4HPzNxbHkdRJbz2lrqI7S+z17xNYdFcBBO8Hw==", + "license": "MIT", + "dependencies": { + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-pluck": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/arr-pluck/-/arr-pluck-0.1.0.tgz", + "integrity": "sha512-r+XGzphTuhTu//mwL9wIjXawJCiKkZqUDgJsUxzq+YGiYb4Gg9+GuIVorvSo7halsbEiDj5D34cquiHj7jTvgg==", + "license": "MIT", + "dependencies": { + "arr-map": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes/node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-sort": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-0.1.4.tgz", + "integrity": "sha512-BNcM+RXxndPxiZ2rd76k6nyQLRZr2/B/sdi8pQ+Joafr5AH279L40dfokSUTp8O+AaqYjXWhblBWa2st2nc4fQ==", + "license": "MIT", + "dependencies": { + "default-compare": "^1.0.0", + "get-value": "^2.0.6", + "kind-of": "^5.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-sort/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha512-G2n5bG5fSUCpnsXz4+8FUkYsGPkNfLn9YvS66U5qbTIXI2Ynnlo4Bi42bWv+omKUCqz+ejzfClwne0alJWJPhg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast/node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex/node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat/node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap/node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array.prototype.tosorted/node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice/node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arrayify-compact": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/arrayify-compact/-/arrayify-compact-0.2.0.tgz", + "integrity": "sha512-uCIqMaBeu+onuiFS1kB2raQYLETAAeWwAGwrZs7soA1nu4TuHfejWJMoFL06SvWHZAxmOCN7UDzcBjUZ6Y6s6Q==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/assemble-core": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/assemble-core/-/assemble-core-0.25.0.tgz", + "integrity": "sha512-5vS/XZK0ke3gIHoKTyl88brqOR9zw3niz5jJHrEgrDLlZGEri4a1Wr4badallKCx4M4/TWG12GT/O5wABZjaVA==", + "license": "MIT", + "dependencies": { + "assemble-fs": "^0.6.0", + "assemble-render-file": "^0.7.1", + "assemble-streams": "^0.6.0", + "base-task": "^0.6.1", + "define-property": "^0.2.5", + "lazy-cache": "^2.0.1", + "templates": "^0.24.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/assemble-fs": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/assemble-fs/-/assemble-fs-0.6.0.tgz", + "integrity": "sha512-vp9szLsFTz0NFa7aiCBZ4JJZPsRRjLB7ftj3anSm/apE+DJ8d1s7kaVFHpxc2LCrEVIGMc1ALLyfRYJDwtzfaw==", + "license": "MIT", + "dependencies": { + "assemble-handle": "^0.1.2", + "extend-shallow": "^2.0.1", + "is-valid-app": "^0.2.0", + "lazy-cache": "^2.0.1", + "stream-combiner": "^0.2.2", + "through2": "^2.0.1", + "vinyl-fs": "^2.4.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/assemble-handle": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/assemble-handle/-/assemble-handle-0.1.4.tgz", + "integrity": "sha512-7O1lbkR2fMqsGwrtGzHraLQHN0OKukPeLF/qgD7yTzFKSKg/HH2xeEN8mKutwymXRzVsUF3AvboJoOjMGiT+5g==", + "license": "MIT", + "dependencies": { + "through2": "^2.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/assemble-loader": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/assemble-loader/-/assemble-loader-0.6.1.tgz", + "integrity": "sha512-jef7ecixuK8DgP2LMJ5TO1Zs6YnltxQN8KDLDYLav+VbfK7+BGVLHv2NNrIm0/Mls2CklNmMqeWcccdSUNRUnQ==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "file-contents": "^0.2.4", + "fs-exists-sync": "^0.1.0", + "has-glob": "^0.1.1", + "is-registered": "^0.1.5", + "is-valid-glob": "^0.3.0", + "is-valid-instance": "^0.1.0", + "isobject": "^2.1.0", + "lazy-cache": "^2.0.1", + "load-templates": "^0.11.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/assemble-render-file": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/assemble-render-file/-/assemble-render-file-0.7.2.tgz", + "integrity": "sha512-Fmt/7KDIwHr/zIStwzl1QEzeph++eP0I7G3tQch1s0ftBllEwZZ5Py7IpO1WPkP+ef8xMRjXNrNKx8/cpTgb4w==", + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "is-valid-app": "^0.1.2", + "lazy-cache": "^2.0.1", + "mixin-deep": "^1.1.3", + "through2": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/assemble-render-file/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/assemble-render-file/node_modules/is-valid-app": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/is-valid-app/-/is-valid-app-0.1.2.tgz", + "integrity": "sha512-UKIjincKieawS6pPJjpH76qUmblicLSi0pqGCvFdscOM3pWgnrRBtB/iWIRYXKNCW8qjxb+6k12wFd82Kq94CA==", + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "is-registered": "^0.1.5", + "is-valid-instance": "^0.1.0", + "lazy-cache": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/assemble-render-file/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/assemble-streams": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/assemble-streams/-/assemble-streams-0.6.0.tgz", + "integrity": "sha512-JEZRYrkAQHKCT41jTVXQ63AxeYGD9aDuxRDZhZH5fsVfvLZGOHXsGPSJBEfDuC6Nz6APJGt9lwWfZH9lqmG65Q==", + "license": "MIT", + "dependencies": { + "assemble-handle": "^0.1.2", + "is-registered": "^0.1.4", + "is-valid-instance": "^0.1.0", + "lazy-cache": "^2.0.1", + "match-file": "^0.2.0", + "src-stream": "^0.1.1", + "through2": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/assign-deep": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/assign-deep/-/assign-deep-0.4.8.tgz", + "integrity": "sha512-uxqXJCnNZDEjPnsaLKVzmh/ST5+Pqoz0wi06HDfHKx1ASNpSbbvz2qW2Gl8ZyHwr5jnm11X2S5eMQaP1lMZmCg==", + "license": "MIT", + "dependencies": { + "assign-symbols": "^0.1.1", + "is-primitive": "^2.0.0", + "kind-of": "^5.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/assign-deep/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/assign-symbols": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-0.1.1.tgz", + "integrity": "sha512-gwzH8QS/GV4pQsf6XOrlpBC6aDE8uJeZvymbEJ0W9TuDYqYOZc4RodvKDH98HCc+KFPYil1kD2XT0X0JWeOzQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "license": "MIT" + }, + "node_modules/async-array-reduce": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/async-array-reduce/-/async-array-reduce-0.2.1.tgz", + "integrity": "sha512-/ywTADOcaEnwiAnOEi0UB/rAcIq5bTFfCV9euv3jLYFUMmy6KvKccTQUnLlp8Ensmfj43wHSmbGiPqjsZ6RhNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async-done": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz", + "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.2", + "process-nextick-args": "^2.0.0", + "stream-exhaust": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/async-each": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.6.tgz", + "integrity": "sha512-c646jH1avxr+aVpndVMeAfYw7wAa6idufrlN3LPA4PmKS0QEGp6PIC9nwz0WQkkvBGAMEki3pFdtxaF39J9vvg==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, + "node_modules/async-each-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-1.1.0.tgz", + "integrity": "sha512-/VIpPVIJJlJObJiXkHBJ1RhjDtydBRG/3/dWpsXoVGOShNw5tameXnC7Yys+wpb0p/myItxGmSGgNi/dNlsIiA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-helpers": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/async-helpers/-/async-helpers-0.3.17.tgz", + "integrity": "sha512-LfgCyvmK6ZiC7pyqOgli2zfkWL4HYbEb+HXvGgdmqVBgsOOtQz5rSF8Ii/H/1cNNtrfj1KsdZE/lUMeIY3Qcwg==", + "license": "MIT", + "dependencies": { + "co": "^4.6.0", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/async-helpers/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async-settle": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-0.2.1.tgz", + "integrity": "sha512-3b4i8Bf/9Zw3V/EsLtMx+qj2r0mDYotjMhzXJQxjvESOe5LgevY5KaH5BHROVZWHE7TlSY2FkeTgIgDvdkRFYQ==", + "license": "MIT", + "dependencies": { + "async-done": "^0.4.0" + } + }, + "node_modules/async-settle/node_modules/async-done": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/async-done/-/async-done-0.4.0.tgz", + "integrity": "sha512-NcrnJY08hBDUa3qhZIfRALshlau6U/Q9X1WHA53t/8OfJpQz5qXPKGFVHwIY38md62TiM9JA+5tpRed5LFWrKw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^0.1.4", + "next-tick": "^0.2.2", + "once": "^1.3.0", + "stream-exhaust": "^1.0.0" + } + }, + "node_modules/async-settle/node_modules/end-of-stream": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-0.1.5.tgz", + "integrity": "sha512-go5TQkd0YRXYhX+Lc3UrXkoKU5j+m72jEP5lHWr2Nh82L8wfZtH8toKgcg4T10o23ELIMGXQdwCbl+qAXIPDrw==", + "license": "MIT", + "dependencies": { + "once": "~1.3.0" + } + }, + "node_modules/async-settle/node_modules/once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.1", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bach": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/bach/-/bach-0.5.0.tgz", + "integrity": "sha512-wr1KICs4sa/Ye4D38CEWkxmRi0E/1NnlcTXE4WT46993f+m+W8rVeRlQVh7O9jUHd3/cyNttv4qIDEUullFPcw==", + "license": "MIT", + "dependencies": { + "async-done": "^1.1.1", + "async-settle": "^0.2.1", + "lodash.filter": "^4.1.0", + "lodash.flatten": "^4.0.0", + "lodash.foreach": "^4.0.0", + "lodash.initial": "^4.0.1", + "lodash.last": "^3.0.0", + "lodash.map": "^4.1.0", + "now-and-later": "0.0.6" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "license": "MIT", + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-argv": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/base-argv/-/base-argv-0.4.5.tgz", + "integrity": "sha512-U78T4In2FMtSYBaf3utKCAOrOBJJXgvGLUmck71ZLQuJZBO6+DDUFoJGfuys0bX/wSQOZgB/HLLFiapvvUUFlw==", + "license": "MIT", + "dependencies": { + "arr-diff": "^2.0.0", + "arr-union": "^3.1.0", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "expand-args": "^0.4.1", + "extend-shallow": "^2.0.1", + "lazy-cache": "^1.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-argv/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-argv/node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-argv/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base-cli": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/base-cli/-/base-cli-0.5.0.tgz", + "integrity": "sha512-GQnPyusKASZoCKR3JFf4iVygLvZjk6RwEQokZF35M9VHnhkoPycf22jYlWkwLEtCejtcLECgGC7fq0G/ab5k8g==", + "license": "MIT", + "dependencies": { + "base-argv": "^0.4.2", + "base-config": "^0.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-cli-process": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/base-cli-process/-/base-cli-process-0.1.19.tgz", + "integrity": "sha512-hH9MGqad9bZBmowsZ8uKL91rS4L+q4GEOc5SaL045jQWaR93sla0UI4Q9C6GzOD2AgVJulY2QtCMmwcBhdVYtQ==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "arrayify-compact": "^0.2.0", + "base-cli": "^0.5.0", + "base-cli-schema": "^0.1.19", + "base-config-process": "^0.1.9", + "base-cwd": "^0.3.4", + "base-option": "^0.8.4", + "base-pkg": "^0.2.4", + "debug": "^2.6.2", + "export-files": "^2.1.1", + "fs-exists-sync": "^0.1.0", + "is-valid-app": "^0.2.1", + "kind-of": "^3.1.0", + "lazy-cache": "^2.0.2", + "log-utils": "^0.2.1", + "merge-deep": "^3.0.0", + "mixin-deep": "^1.2.0", + "object.pick": "^1.2.0", + "pad-right": "^0.2.2", + "union-value": "^1.0.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/base-cli-process/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-cli-process/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base-cli-schema": { + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/base-cli-schema/-/base-cli-schema-0.1.19.tgz", + "integrity": "sha512-8k3JPZjVjdwpYtaaF3F8JT9RztX1oFDWKsAVDpUUR/uXL6b85DyTpRX4TUw3rjwZMZIf1BmiTys2zOSqC7+oAA==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "array-unique": "^0.2.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "export-files": "^2.1.1", + "extend-shallow": "^2.0.1", + "falsey": "^0.3.0", + "fs-exists-sync": "^0.1.0", + "has-glob": "^0.1.1", + "has-value": "^0.3.1", + "kind-of": "^3.0.3", + "lazy-cache": "^2.0.1", + "map-schema": "^0.2.3", + "merge-deep": "^3.0.0", + "mixin-deep": "^1.1.3", + "resolve": "^1.1.7", + "tableize-object": "^0.1.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/base-cli-schema/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-cli-schema/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base-compose": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/base-compose/-/base-compose-0.2.1.tgz", + "integrity": "sha512-z/wx9ij4i4Bj6WbXJeJlVO2O99eErMXSWjyYUt/NAfxrGpNfMz4SWS9P0OYx9RVQ2CyMEcT1J3z5+9EqQQr8Ug==", + "license": "MIT", + "dependencies": { + "copy-task": "^0.1.0", + "lazy-cache": "^2.0.1", + "mixin-deep": "^1.1.3" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/base-config": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/base-config/-/base-config-0.5.2.tgz", + "integrity": "sha512-Oq0PKM//Sh82mHQt64eUi5GZQOM8I+aNkM/P8Al4A5qwaGBkxKB+ElNqJHUVlF3WA9VjBLYUmO9asGzLEigxBw==", + "license": "MIT", + "dependencies": { + "isobject": "^2.0.0", + "lazy-cache": "^1.0.3", + "map-config": "^0.5.0", + "resolve-dir": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-config-process": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/base-config-process/-/base-config-process-0.1.9.tgz", + "integrity": "sha512-tShRbXNMml5V/qgcZ3ntWsaS6ovw1t7e4yvtYY9XzhJtNpuC8WudMwtSbG7lXAuEZ04jY1istJzKR3NzAoxo3A==", + "license": "MIT", + "dependencies": { + "base-config": "^0.5.2", + "base-config-schema": "^0.1.18", + "base-cwd": "^0.3.4", + "base-option": "^0.8.4", + "debug": "^2.2.0", + "export-files": "^2.1.1", + "is-valid-app": "^0.2.0", + "lazy-cache": "^2.0.1", + "micromatch": "^2.3.10", + "mixin-deep": "^1.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-config-process/node_modules/braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha512-xU7bpz2ytJl1bH9cgIurjpg/n8Gohy9GTw81heDYLJQ4RU60dlyJsa+atVF2pI0yMMvKxI9HkKwjePCj5XI1hw==", + "license": "MIT", + "dependencies": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-config-process/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-config-process/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-config-process/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-config-process/node_modules/micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha512-LnU2XFEk9xxSJ6rfgAry/ty5qwUTyHYOBU0g4R6tIw5ljwgGIBmiKhRWLw5NpMOnrgUNcDJ4WMp8rl3sYVHLNA==", + "license": "MIT", + "dependencies": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-config-process/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base-config-process/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-config-schema": { + "version": "0.1.24", + "resolved": "https://registry.npmjs.org/base-config-schema/-/base-config-schema-0.1.24.tgz", + "integrity": "sha512-3CYvd28nsiNVp1rkAfVqfYo7VzDPdIxwv0Ab6iGY0K7JdGRsT6U7Jqq6BBMGNd9XLazLhVBPNGUzaDg5oUtV5w==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.3", + "array-unique": "^0.3.2", + "base-pkg": "^0.2.4", + "camel-case": "^3.0.0", + "debug": "^2.6.6", + "define-property": "^1.0.0", + "export-files": "^2.1.1", + "extend-shallow": "^2.0.1", + "has-glob": "^1.0.0", + "has-value": "^0.3.1", + "inflection": "^1.12.0", + "kind-of": "^3.2.0", + "lazy-cache": "^2.0.2", + "load-templates": "^1.0.2", + "map-schema": "^0.2.4", + "matched": "^0.4.4", + "mixin-deep": "^1.2.0", + "resolve": "^1.3.3" + }, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/base-config-schema/node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-config-schema/node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/base-config-schema/node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", + "license": "MIT" + }, + "node_modules/base-config-schema/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-config-schema/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-config-schema/node_modules/file-contents": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/file-contents/-/file-contents-1.0.1.tgz", + "integrity": "sha512-yR9NGsF6Ua0vUjag441JRYB+WflAoBCF3+ReeKocYzpfAjN1U4TvQEjIKXOqwIxFl9Bflg8xf/Fi2qrNBoFUOQ==", + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "is-buffer": "^1.1.4", + "kind-of": "^3.1.0", + "lazy-cache": "^2.0.2", + "strip-bom-buffer": "^0.1.1", + "strip-bom-string": "^0.1.2", + "through2": "^2.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-config-schema/node_modules/file-contents/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-config-schema/node_modules/file-contents/node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/base-config-schema/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "license": "ISC", + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/base-config-schema/node_modules/has-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-glob/-/has-glob-1.0.0.tgz", + "integrity": "sha512-D+8A457fBShSEI3tFCj65PAbT++5sKiFtdCdOam0gnfBgw9D277OERk+HM9qYJXmdVLZ/znez10SqHN0BBQ50g==", + "license": "MIT", + "dependencies": { + "is-glob": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-config-schema/node_modules/is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/base-config-schema/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-config-schema/node_modules/load-templates": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/load-templates/-/load-templates-1.0.2.tgz", + "integrity": "sha512-UUfhwRTBH9V4Uf0gGX7FqU5RUdi9IvJWrY1AaPRCRkV/LE/cbudUtY0+YXZs1fNp1J4PFlwOMyrtfzSOCtBbJA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "file-contents": "^1.0.0", + "glob-parent": "^3.1.0", + "is-glob": "^3.1.0", + "kind-of": "^3.1.0", + "lazy-cache": "^2.0.2", + "matched": "^0.4.4", + "vinyl": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-config-schema/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base-config-schema/node_modules/replace-ext": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", + "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/base-config-schema/node_modules/strip-bom-string": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-0.1.2.tgz", + "integrity": "sha512-3DgNqQFTfOwWgxn3cXsa6h/WRgFa7dVb6/7YqwfJlBpLSSQbiU1VhaBNRKmtLI59CHjc9awLp9yGJREu7AnaMQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-config-schema/node_modules/vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "license": "MIT", + "dependencies": { + "clone": "^2.1.1", + "clone-buffer": "^1.0.0", + "clone-stats": "^1.0.0", + "cloneable-readable": "^1.0.0", + "remove-trailing-separator": "^1.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/base-config/node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-cwd": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/base-cwd/-/base-cwd-0.3.4.tgz", + "integrity": "sha512-/kxZE1Hg9p4tvy4DHrWyS/DelZeovOWvBZ9CZKTgeieIxMuZ47FaLIkEkcjOVFcu3nIY4TXdlxhMZFi8D2Rs9g==", + "license": "MIT", + "dependencies": { + "empty-dir": "^0.2.0", + "find-pkg": "^0.1.2", + "is-valid-app": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-data": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/base-data/-/base-data-0.6.2.tgz", + "integrity": "sha512-wH2ViG6CUO2AaeHSEt6fJTyQAk5gl0oY456DoSC5h8mnHrWUbvdctMCuF53CXgBmi0oalZQppKNH0iamG5+uqw==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.1.0", + "cache-base": "^1.0.0", + "extend-shallow": "^2.0.1", + "get-value": "^2.0.6", + "has-glob": "^1.0.0", + "has-value": "^1.0.0", + "is-registered": "^0.1.5", + "is-valid-app": "^0.3.0", + "kind-of": "^5.0.0", + "lazy-cache": "^2.0.2", + "merge-value": "^1.0.0", + "mixin-deep": "^1.2.0", + "read-file": "^0.2.0", + "resolve-glob": "^1.0.0", + "set-value": "^2.0.0", + "union-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-data/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-data/node_modules/has-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-glob/-/has-glob-1.0.0.tgz", + "integrity": "sha512-D+8A457fBShSEI3tFCj65PAbT++5sKiFtdCdOam0gnfBgw9D277OERk+HM9qYJXmdVLZ/znez10SqHN0BBQ50g==", + "license": "MIT", + "dependencies": { + "is-glob": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-data/node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "license": "MIT", + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-data/node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "license": "MIT", + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-data/node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-data/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-data/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-data/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-data/node_modules/is-valid-app": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-valid-app/-/is-valid-app-0.3.0.tgz", + "integrity": "sha512-6+PklNvJraE3XpoqWurkrPIqFIeJin5kwX+sJjcwhPcFY7TM0wjbJlPIBCvHtGawIfb4WtS1t22s7TdgQ0S+Xg==", + "license": "MIT", + "dependencies": { + "debug": "^2.6.3", + "is-registered": "^0.1.5", + "is-valid-instance": "^0.3.0", + "lazy-cache": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-data/node_modules/is-valid-instance": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-valid-instance/-/is-valid-instance-0.3.0.tgz", + "integrity": "sha512-XEd0ddnORLW/Qf1+VMh7PnYb6XhWs0zK0C/Kh8muwj26IjdlCTlo7QQIjt8+efkE8RqtyzlqYNZE5SfN8ys9hQ==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-data/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-data/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-data/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base-engines": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/base-engines/-/base-engines-0.2.1.tgz", + "integrity": "sha512-s/A07Vbh6irEMNG+HpccmaGw8SUMXPBetJuYPpq7Rf1WCjtCU1L+FKyeKyRahONGNYBSIHEV0d3cqXYw35EjBw==", + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "define-property": "^0.2.5", + "engine-cache": "^0.19.0", + "is-valid-app": "^0.1.2", + "lazy-cache": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-engines/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-engines/node_modules/is-valid-app": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/is-valid-app/-/is-valid-app-0.1.2.tgz", + "integrity": "sha512-UKIjincKieawS6pPJjpH76qUmblicLSi0pqGCvFdscOM3pWgnrRBtB/iWIRYXKNCW8qjxb+6k12wFd82Kq94CA==", + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "is-registered": "^0.1.5", + "is-valid-instance": "^0.1.0", + "lazy-cache": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-engines/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base-env": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/base-env/-/base-env-0.3.1.tgz", + "integrity": "sha512-/HxC8QV1m/bWqvjcu4WZl4Um1HRpTAjuY31uiFUEukXsXge4WIvNvGKG/gCs2PrpBFPCybowA406V/ivdPknpQ==", + "license": "MIT", + "dependencies": { + "base-namespace": "^0.2.0", + "contains-path": "^0.1.0", + "debug": "^2.2.0", + "extend-shallow": "^2.0.1", + "fs-exists-sync": "^0.1.0", + "global-modules": "^0.2.2", + "is-absolute": "^0.2.5", + "is-valid-app": "^0.1.0", + "is-valid-instance": "^0.1.0", + "kind-of": "^3.0.3", + "os-homedir": "^1.0.1", + "resolve-file": "^0.3.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-env/node_modules/cwd": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", + "integrity": "sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA==", + "license": "MIT", + "dependencies": { + "find-pkg": "^0.1.2", + "fs-exists-sync": "^0.1.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/base-env/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-env/node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-env/node_modules/is-valid-app": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/is-valid-app/-/is-valid-app-0.1.2.tgz", + "integrity": "sha512-UKIjincKieawS6pPJjpH76qUmblicLSi0pqGCvFdscOM3pWgnrRBtB/iWIRYXKNCW8qjxb+6k12wFd82Kq94CA==", + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "is-registered": "^0.1.5", + "is-valid-instance": "^0.1.0", + "lazy-cache": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-env/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base-env/node_modules/resolve-file": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/resolve-file/-/resolve-file-0.3.0.tgz", + "integrity": "sha512-9RXicAgDvLD272hZ3HwJv9MJUGxCBRRwwSBRdOGWgcO03MtC9UTGC6XG1VbS4T5MvDrb+tVZx2RhZ90uk3uczg==", + "license": "MIT", + "dependencies": { + "cwd": "^0.10.0", + "expand-tilde": "^2.0.2", + "extend-shallow": "^2.0.1", + "fs-exists-sync": "^0.1.0", + "homedir-polyfill": "^1.0.1", + "lazy-cache": "^2.0.2", + "resolve": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-generators": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/base-generators/-/base-generators-0.4.6.tgz", + "integrity": "sha512-0k8QAoqYhOwIHQANQxwNOhtlQiuoMqv+rFu2szVIvLUNhZ8B7BOXWFRE5UXMAexRxz7H8rZIwLmeqxlYpOXJGw==", + "license": "MIT", + "dependencies": { + "async-each-series": "^1.1.0", + "base-compose": "^0.2.1", + "base-cwd": "^0.3.1", + "base-data": "^0.6.0", + "base-env": "^0.3.0", + "base-option": "^0.8.4", + "base-pkg": "^0.2.4", + "base-plugins": "^0.4.13", + "base-task": "^0.6.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "global-modules": "^0.2.2", + "is-valid-app": "^0.2.0", + "is-valid-instance": "^0.2.0", + "kind-of": "^3.0.3", + "lazy-cache": "^2.0.1", + "mixin-deep": "^1.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-generators/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-generators/node_modules/is-valid-instance": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-valid-instance/-/is-valid-instance-0.2.0.tgz", + "integrity": "sha512-dNT7bamkigo07gvbnoBRABSNX1ayAhkcw6/3fYhVDhiPXiqnCouD4JMmrozyOx37UUlC+Se1j/jCfLo1fNs0Ng==", + "license": "MIT", + "dependencies": { + "isobject": "^2.1.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-generators/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base-helpers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/base-helpers/-/base-helpers-0.1.1.tgz", + "integrity": "sha512-aUdOoz47aMdM2OAkN71P3m8wjFB+pZDVfvLebDoNAsD0zhKUc68QR30q9iK6vW6S302yNNVW8bZxUF6FwFLnQw==", + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "define-property": "^0.2.5", + "is-valid-app": "^0.1.0", + "lazy-cache": "^2.0.1", + "load-helpers": "^0.2.11" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-helpers/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-helpers/node_modules/is-valid-app": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/is-valid-app/-/is-valid-app-0.1.2.tgz", + "integrity": "sha512-UKIjincKieawS6pPJjpH76qUmblicLSi0pqGCvFdscOM3pWgnrRBtB/iWIRYXKNCW8qjxb+6k12wFd82Kq94CA==", + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "is-registered": "^0.1.5", + "is-valid-instance": "^0.1.0", + "lazy-cache": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-helpers/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base-namespace": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/base-namespace/-/base-namespace-0.2.0.tgz", + "integrity": "sha512-jZYAnj1wkwyi6HkqATtO86D8L9jbDdqVthISLG27LcXCFkc5EV+BwS/cfaPBkWoMGb3NsVMau+PLfFle58Xi2g==", + "license": "MIT", + "dependencies": { + "is-valid-app": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-namespace/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-namespace/node_modules/is-valid-app": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/is-valid-app/-/is-valid-app-0.1.2.tgz", + "integrity": "sha512-UKIjincKieawS6pPJjpH76qUmblicLSi0pqGCvFdscOM3pWgnrRBtB/iWIRYXKNCW8qjxb+6k12wFd82Kq94CA==", + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "is-registered": "^0.1.5", + "is-valid-instance": "^0.1.0", + "lazy-cache": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-namespace/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base-option": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/base-option/-/base-option-0.8.4.tgz", + "integrity": "sha512-CS9V8trhwEccFFjmveBHWx4Wr4rwaohzMhwZx1DSUHdGHV9Nme3jbxJQ0U8JsrLFJvGtiav35NiHLeNd8n74XA==", + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "get-value": "^2.0.6", + "is-valid-app": "^0.2.0", + "isobject": "^2.1.0", + "lazy-cache": "^2.0.1", + "mixin-deep": "^1.1.3", + "option-cache": "^3.4.0", + "set-value": "^0.3.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-option/node_modules/set-value": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.3.3.tgz", + "integrity": "sha512-aJPTd11HzK47w8xJMpyY4tBmFC6EidC8EG2fENxCJvPwLYzXLnNaesgo796y1fhSISSYAuah4Het+wDoPXK2tg==", + "deprecated": "Critical bug fixed in v3.0.1, please upgrade to the latest version.", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "isobject": "^2.0.0", + "to-object-path": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-option/node_modules/to-object-path": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.2.0.tgz", + "integrity": "sha512-6oMu4CTicplxUMOXBoS1W9YNjIclUzmWpWf02v+JnYMEGVX24rTCsYMHay85WA7Wq+9wZa2iJ+HAAX0yGOcxCQ==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "is-arguments": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-pkg": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/base-pkg/-/base-pkg-0.2.5.tgz", + "integrity": "sha512-/POxajlgBhVsknwLXnqnbp//bAMh7SkDgHF+z/uoYnFqk46e05c3MxSEmn5vFCB8g4rHHKxAPLKrU/4Yb3vUdA==", + "license": "MIT", + "dependencies": { + "cache-base": "^1.0.0", + "debug": "^2.6.8", + "define-property": "^1.0.0", + "expand-pkg": "^0.1.8", + "extend-shallow": "^2.0.1", + "is-valid-app": "^0.3.0", + "log-utils": "^0.2.1", + "pkg-store": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-pkg/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-pkg/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-pkg/node_modules/is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/base-pkg/node_modules/is-valid-app": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-valid-app/-/is-valid-app-0.3.0.tgz", + "integrity": "sha512-6+PklNvJraE3XpoqWurkrPIqFIeJin5kwX+sJjcwhPcFY7TM0wjbJlPIBCvHtGawIfb4WtS1t22s7TdgQ0S+Xg==", + "license": "MIT", + "dependencies": { + "debug": "^2.6.3", + "is-registered": "^0.1.5", + "is-valid-instance": "^0.3.0", + "lazy-cache": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-pkg/node_modules/is-valid-instance": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-valid-instance/-/is-valid-instance-0.3.0.tgz", + "integrity": "sha512-XEd0ddnORLW/Qf1+VMh7PnYb6XhWs0zK0C/Kh8muwj26IjdlCTlo7QQIjt8+efkE8RqtyzlqYNZE5SfN8ys9hQ==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-pkg/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-pkg/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base-plugins": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/base-plugins/-/base-plugins-0.4.13.tgz", + "integrity": "sha512-w77IDOnkxERPZ7x27A8MmSFcwEfTfrcZ43zK5eOt42itA8FZT9OFhZm1XgOtTEORKrCmW8yVT6DWr/ut7wvgiQ==", + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "is-registered": "^0.1.5", + "isobject": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-questions": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/base-questions/-/base-questions-0.7.4.tgz", + "integrity": "sha512-uHRp5ZM2MFXUhDOPK09lroJdDe3lrXTHtg2x7pC1x4RdimVZcsX+hvQuxNqyAUN62EHfFuaK+FIFjMiA4AoiQg==", + "license": "MIT", + "dependencies": { + "base-store": "^0.4.4", + "clone-deep": "^0.2.4", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "is-valid-app": "^0.2.0", + "isobject": "^2.1.0", + "lazy-cache": "^2.0.1", + "mixin-deep": "^1.1.3", + "question-store": "^0.11.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-questions/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-questions/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base-routes": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/base-routes/-/base-routes-0.2.2.tgz", + "integrity": "sha512-z7jtXacfUbjAKUGj5jmJP8GrhZG+UqcwnfkKjLJtUa1w1bWrq5JmsZ1SFRfomXWbLAlEcE87dHvelvTkelQBIg==", + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "en-route": "^0.7.5", + "is-valid-app": "^0.2.0", + "lazy-cache": "^2.0.1", + "template-error": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-routes/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-routes/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base-runtimes": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/base-runtimes/-/base-runtimes-0.2.0.tgz", + "integrity": "sha512-J98SbWB4Rpcva8w8kWtTts+Qc/X/imcmFoy9nt2fKemPTmVgvrt8DyDK5KFUDyQHt+hahYa69pJTGFfUma7V8A==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-valid-app": "^0.2.0", + "lazy-cache": "^2.0.1", + "log-utils": "^0.1.4", + "micromatch": "^2.3.10", + "time-diff": "^0.3.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-runtimes/node_modules/ansi-colors": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-0.1.0.tgz", + "integrity": "sha512-nUNbMZLDr1YQaPdMC2lREJXKttoaHwICajt9x40Js/POX7gNv7OK/VbC9ciJaIFshg9Xol+1GclqfY14UW+0ZA==", + "license": "MIT", + "dependencies": { + "ansi-bgblack": "^0.1.1", + "ansi-bgblue": "^0.1.1", + "ansi-bgcyan": "^0.1.1", + "ansi-bggreen": "^0.1.1", + "ansi-bgmagenta": "^0.1.1", + "ansi-bgred": "^0.1.1", + "ansi-bgwhite": "^0.1.1", + "ansi-bgyellow": "^0.1.1", + "ansi-black": "^0.1.1", + "ansi-blue": "^0.1.1", + "ansi-bold": "^0.1.1", + "ansi-cyan": "^0.1.1", + "ansi-dim": "^0.1.1", + "ansi-gray": "^0.1.1", + "ansi-green": "^0.1.1", + "ansi-grey": "^0.1.1", + "ansi-hidden": "^0.1.1", + "ansi-inverse": "^0.1.1", + "ansi-italic": "^0.1.1", + "ansi-magenta": "^0.1.1", + "ansi-red": "^0.1.1", + "ansi-reset": "^0.1.1", + "ansi-strikethrough": "^0.1.1", + "ansi-underline": "^0.1.1", + "ansi-white": "^0.1.1", + "ansi-yellow": "^0.1.1", + "lazy-cache": "^0.2.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-runtimes/node_modules/ansi-colors/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-runtimes/node_modules/braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha512-xU7bpz2ytJl1bH9cgIurjpg/n8Gohy9GTw81heDYLJQ4RU60dlyJsa+atVF2pI0yMMvKxI9HkKwjePCj5XI1hw==", + "license": "MIT", + "dependencies": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-runtimes/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-runtimes/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-runtimes/node_modules/log-utils": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/log-utils/-/log-utils-0.1.5.tgz", + "integrity": "sha512-5jLIj9RWWYxQbBhHDvNZTZE3J/oSTbw/fuPmsXJg8/vbY/4XiJ4YAiEPrwo3dLbcB/n9k1qTznOVr6IigiaF7A==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^0.1.0", + "error-symbol": "^0.1.0", + "info-symbol": "^0.1.0", + "log-ok": "^0.1.1", + "success-symbol": "^0.1.0", + "time-stamp": "^1.0.1", + "warning-symbol": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-runtimes/node_modules/micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha512-LnU2XFEk9xxSJ6rfgAry/ty5qwUTyHYOBU0g4R6tIw5ljwgGIBmiKhRWLw5NpMOnrgUNcDJ4WMp8rl3sYVHLNA==", + "license": "MIT", + "dependencies": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-runtimes/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-store": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/base-store/-/base-store-0.4.4.tgz", + "integrity": "sha512-fb5L2iNR9pCl85jeg88TCJYlcKg8xhmdH1Cjp1MI2RZNnMBjdIaQOuGy9Q4VjSD/GNGBWgQ2H8pQK61Xsx29OA==", + "license": "MIT", + "dependencies": { + "data-store": "^0.16.0", + "debug": "^2.2.0", + "extend-shallow": "^2.0.1", + "is-registered": "^0.1.4", + "is-valid-instance": "^0.1.0", + "lazy-cache": "^2.0.1", + "project-name": "^0.2.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-store/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-store/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base-task": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/base-task/-/base-task-0.6.2.tgz", + "integrity": "sha512-dxCXKPLFRrl02kJ+Lu6Y0Y2/XeaVf3GbGXMoZKuHN9OvFjz+QXRwpTJ0PciQPAvktUgK46Mc9Kwakrcj8fSTog==", + "license": "MIT", + "dependencies": { + "composer": "^0.13.0", + "is-valid-app": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-task/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/base-task/node_modules/is-valid-app": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/is-valid-app/-/is-valid-app-0.1.2.tgz", + "integrity": "sha512-UKIjincKieawS6pPJjpH76qUmblicLSi0pqGCvFdscOM3pWgnrRBtB/iWIRYXKNCW8qjxb+6k12wFd82Kq94CA==", + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "is-registered": "^0.1.5", + "is-valid-instance": "^0.1.0", + "lazy-cache": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base-task/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/base/node_modules/is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/base/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.10", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "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" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "license": "MIT", + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cache-base/node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "license": "MIT", + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cache-base/node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "license": "MIT", + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cache-base/node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cache-base/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cache-base/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", + "license": "MIT", + "dependencies": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "node_modules/camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "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" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-utils/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha512-25tABq090YNKkF6JH7lcwO0zFJTRke4Jcq9iX2nr/Sz0Cjjv4gckmwlW6Ty/aoyFd6z3ysR2hMGC2GFugmBo6A==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cli-width": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-1.1.1.tgz", + "integrity": "sha512-eMU2akIeEIkCxGXUNmDnJq1KzOIiPnJ+rKqRe6hcxE3vIOPvpMrBYOn/Bl7zNlYJj/zQxXquAnozHUCf9Whnsg==", + "license": "ISC" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/clone-deep": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==", + "license": "MIT", + "dependencies": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clone-deep/node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha512-dhUqc57gSMCo6TX85FLfe51eC/s+Im2MLkAgJwfaRRexR2tA4dd3eLEW4L6efzHc2iNorrRRXITifnDLlRrhaA==", + "license": "MIT" + }, + "node_modules/cloneable-readable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", + "license": "MIT", + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/common-config": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/common-config/-/common-config-0.1.1.tgz", + "integrity": "sha512-mDp+nqoFbYsHKZfjg8OSb0CYfdPkuoGTMCVKy4ceYHR0EACTLV/qG8Q4cih2c/0IleQ7SISiqWqLMLXXZnJ2FA==", + "license": "MIT", + "dependencies": { + "composer": "^0.13.0", + "data-store": "^0.16.1", + "get-value": "^2.0.6", + "lazy-cache": "^2.0.1", + "log-utils": "^0.2.0", + "object.pick": "^1.1.2", + "omit-empty": "^0.4.1", + "question-cache": "^0.4.0", + "set-value": "^3.0.1", + "strip-color": "^0.1.0", + "tableize-object": "^0.1.0", + "text-table": "^0.2.0", + "yargs-parser": "^2.4.0" + }, + "bin": { + "common-config": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/common-config/node_modules/set-value": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-3.0.3.tgz", + "integrity": "sha512-Xsn/XSatoVOGBbp5hs3UylFDs5Bi9i+ArpVJKdHPniZHoEgRniXTqHWrWrGQ0PbEClVT6WtfnBwR8CAHC9sveg==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/composer": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/composer/-/composer-0.13.0.tgz", + "integrity": "sha512-8bW8vzd0YdwjBTbbHmUV3fb1jGFlczUEwti3dbdogI+r/igv2yyLqZFh9IyQv4+gK3k1kdNGVrf6Af5BY8qB3Q==", + "license": "MIT", + "dependencies": { + "array-unique": "^0.2.1", + "bach": "^0.5.0", + "co": "^4.6.0", + "component-emitter": "^1.2.1", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "is-generator": "^1.0.3", + "is-glob": "^2.0.1", + "isobject": "^2.1.0", + "lazy-cache": "^2.0.1", + "micromatch": "^2.3.8", + "nanoseconds": "^0.1.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/composer/node_modules/braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha512-xU7bpz2ytJl1bH9cgIurjpg/n8Gohy9GTw81heDYLJQ4RU60dlyJsa+atVF2pI0yMMvKxI9HkKwjePCj5XI1hw==", + "license": "MIT", + "dependencies": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/composer/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/composer/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/composer/node_modules/micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha512-LnU2XFEk9xxSJ6rfgAry/ty5qwUTyHYOBU0g4R6tIw5ljwgGIBmiKhRWLw5NpMOnrgUNcDJ4WMp8rl3sYVHLNA==", + "license": "MIT", + "dependencies": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/composer/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "license": "MIT" + }, + "node_modules/contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha512-OKZnPGeMQy2RPaUIBPFFd71iNf4791H12MCRuVQDnzGRwCYNYmTDy5pdafo2SLAcEMKzTOQnLWG4QdcjeJUMEg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/copy-task": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/copy-task/-/copy-task-0.1.0.tgz", + "integrity": "sha512-Idcf7BdeyJY8kSQodguY8jevkP8CuB22S9Hr5blRqwEyO75yuZEJQbzJ755Q9vZREnCQ5sfOIRxjZWbUq2+K0g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/cwd": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.9.1.tgz", + "integrity": "sha512-4+0D+ojEasdLndYX4Cqff057I/Jp6ysXpwKkdLQLnZxV8f6IYZmZtTP5uqD91a/kWqejoc0sSqK4u8wpTKCh8A==", + "license": "MIT", + "dependencies": { + "find-pkg": "^0.1.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-store": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/data-store/-/data-store-0.16.1.tgz", + "integrity": "sha512-tGbl4oVi9UPysie6y6+fuCjUNhaR3KxnuIRV0OMUCwq/wvikmWHXQYALbW/IVQvmxBNbrxUwjG5BWsrjx5v55w==", + "license": "MIT", + "dependencies": { + "cache-base": "^0.8.4", + "clone-deep": "^0.2.4", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "graceful-fs": "^4.1.4", + "has-own-deep": "^0.1.4", + "lazy-cache": "^2.0.1", + "mkdirp": "^0.5.1", + "project-name": "^0.2.5", + "resolve-dir": "^0.1.0", + "rimraf": "^2.5.3", + "union-value": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/data-store/node_modules/cache-base": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-0.8.5.tgz", + "integrity": "sha512-19t0n7xdoVr5Q08+6sF85YZ9VuvbpVFq5JLm0gcsRmCvTO1Y3duTJGMaOQYf14Ras4o6dEnvoqvjdrUK1tNtgg==", + "license": "MIT", + "dependencies": { + "collection-visit": "^0.2.1", + "component-emitter": "^1.2.1", + "get-value": "^2.0.5", + "has-value": "^0.3.1", + "isobject": "^3.0.0", + "lazy-cache": "^2.0.1", + "set-value": "^0.4.2", + "to-object-path": "^0.3.0", + "union-value": "^0.2.3", + "unset-value": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/data-store/node_modules/collection-visit": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-0.2.3.tgz", + "integrity": "sha512-V88PJOCqJfsZS45YBELDgmhQkECokQAAr9XR4hT6eFkFsAPsCsk3EoDHSuBPYzygjquGM/0KF4vdwTiQO6lbdw==", + "license": "MIT", + "dependencies": { + "lazy-cache": "^2.0.1", + "map-visit": "^0.1.5", + "object-visit": "^0.3.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/data-store/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/data-store/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/data-store/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/data-store/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/data-store/node_modules/map-visit": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-0.1.5.tgz", + "integrity": "sha512-zdmJBFvvVR/H5wCfsCP7XxSLp+346yAZ30Wy2OsQLcH19OVGMWa3Ms9quO00lj9ybsySu3gKOINNgICb4Zqauw==", + "license": "MIT", + "dependencies": { + "lazy-cache": "^2.0.1", + "object-visit": "^0.3.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/data-store/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/data-store/node_modules/object-visit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-0.3.4.tgz", + "integrity": "sha512-6QNyX7uTuwqxP7pmDBqgBDKdmZws1rXriUyXM5KG6+7J0aYRuuAGoc636IGdLzgOL77WUwL+EpoTJrEHwWsyOA==", + "license": "MIT", + "dependencies": { + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/data-store/node_modules/object-visit/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "license": "MIT", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/data-store/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/data-store/node_modules/set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha512-2Z0LRUUvYeF7gIFFep48ksPq0NR09e5oKoFXznaMGNcu+EZAfGnyL0K6xno2gCqX6dZYEZRjrcn04/gvZzcKhQ==", + "deprecated": "Critical bug fixed in v3.0.1, please upgrade to the latest version.", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/data-store/node_modules/union-value": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-0.2.4.tgz", + "integrity": "sha512-Tv3cqdyY8yjW9ZcJ9WP7JdHS34natzylD0oNRLlYbWOfUdC4EQ0sf3fubnqrK2IErtlmobFmuS1pWvv88VghpA==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/data-store/node_modules/unset-value": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-0.1.2.tgz", + "integrity": "sha512-yhv5I4TsldLdE3UcVQn0hD2T5sNCPv4+qm/CTUpRKIpwthYRIipsAPdsrNpOI79hPQa0rTTeW22Fq6JWRcTgNg==", + "license": "MIT", + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-bind": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/deep-bind/-/deep-bind-0.3.0.tgz", + "integrity": "sha512-SwekOBPDnCT3qhOM78ARzBdPSbNMyQ63F8eZDahBzzVAoqousMhYh3HYIh2pLmhtGcVvO8/SU6B6kMsj0SXb1Q==", + "license": "MIT", + "dependencies": { + "mixin-deep": "^1.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/default-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", + "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", + "license": "MIT", + "dependencies": { + "kind-of": "^5.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-compare/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults-deep": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/defaults-deep/-/defaults-deep-0.2.4.tgz", + "integrity": "sha512-V6BtqzcMvn0EPOy7f+SfMhfmTawq+7UQdt9yZH0EBK89+IHo5f+Hse/qzTorAXOBrQpxpwb6cB/8OgtaMrT+Fg==", + "license": "MIT", + "dependencies": { + "for-own": "^0.1.3", + "is-extendable": "^0.1.1", + "lazy-cache": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults-deep/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delimiter-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/delimiter-regex/-/delimiter-regex-2.0.0.tgz", + "integrity": "sha512-EtGkq9TgEZlFACc/NvgwIidQ1wkEupWWbAIJTr9gi4TJUZOvHY8TdXd3i8/dan66BufB1/V6bI7rRW/zvGoVKw==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^1.1.2", + "isobject": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delimiter-regex/node_modules/extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", + "license": "MIT", + "dependencies": { + "kind-of": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delimiter-regex/node_modules/kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.325", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/empty-dir": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/empty-dir/-/empty-dir-0.2.1.tgz", + "integrity": "sha512-0f1naHGJh4K6iVG28nRN7SCdfzT18OlpGzHmXw3JGwREb8qmtibHdmRgqx08u4sQfDadezK7kpU3bcIZNSwoZw==", + "license": "MIT", + "dependencies": { + "fs-exists-sync": "^0.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/en-route": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/en-route/-/en-route-0.7.5.tgz", + "integrity": "sha512-WjnZ2HzvoztSL/NhKYmlN86tSP7VkOTN0Ck4FBJUsvTfLQOlULZak/1wcUArcdenvT9mNS3NzQ+41lqKf/gaGQ==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "debug": "^2.2.0", + "extend-shallow": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "path-to-regexp": "^1.2.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/en-route/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/en-route/node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/en-route/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/engine/-/engine-0.1.12.tgz", + "integrity": "sha512-1+oxmZV5nKFhoR3QkwIbyHKSVbMuNgU8+oxcx4Af1kpxuSjDD0nL3pKKJtY1mGjAPqSAwNeDEHzD94NR5LP5rg==", + "license": "MIT", + "dependencies": { + "assign-deep": "^0.4.3", + "collection-visit": "^0.2.0", + "get-value": "^1.2.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "object.omit": "^2.0.0", + "set-value": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine-base": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/engine-base/-/engine-base-0.1.3.tgz", + "integrity": "sha512-CdNgUJcWgD9OsZ4vDFDmQB1/sN+UM0hEaDcbTZ2Ya/eMTkgCbdRLGvNuRE1UbN+AQJNo8Sm6iT327ULB7ynqnQ==", + "license": "MIT", + "dependencies": { + "component-emitter": "^1.2.1", + "delimiter-regex": "^2.0.0", + "engine": "^0.1.12", + "engine-utils": "^0.1.1", + "lazy-cache": "^2.0.2", + "mixin-deep": "^1.1.3", + "object.omit": "^2.0.1", + "object.pick": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine-cache": { + "version": "0.19.4", + "resolved": "https://registry.npmjs.org/engine-cache/-/engine-cache-0.19.4.tgz", + "integrity": "sha512-PNhE008O6X+7VggZSVe0+fZcafIAjVHWuU+iLIbeKXGGKzjb05Y8ht0l1O9sIusrULRsNq/FcYVPoqoNz7k4wg==", + "license": "MIT", + "dependencies": { + "async-helpers": "^0.3.9", + "extend-shallow": "^2.0.1", + "helper-cache": "^0.7.2", + "isobject": "^3.0.0", + "lazy-cache": "^2.0.2", + "mixin-deep": "^1.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine-cache/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine-utils": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/engine-utils/-/engine-utils-0.1.1.tgz", + "integrity": "sha512-5IdkZiV3qEGS3STfaRfeQsQ93Sokg9cEK7rdfjCGZFY6O/iTdq+d0obwqjkmv4fTSbTqEgYV+J3TeSzkq9GP5A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine/node_modules/collection-visit": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-0.2.3.tgz", + "integrity": "sha512-V88PJOCqJfsZS45YBELDgmhQkECokQAAr9XR4hT6eFkFsAPsCsk3EoDHSuBPYzygjquGM/0KF4vdwTiQO6lbdw==", + "license": "MIT", + "dependencies": { + "lazy-cache": "^2.0.1", + "map-visit": "^0.1.5", + "object-visit": "^0.3.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine/node_modules/collection-visit/node_modules/lazy-cache": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", + "integrity": "sha512-7vp2Acd2+Kz4XkzxGxaB1FWOi8KjWIWsgdfD5MCb86DWvlLqhRPM+d6Pro3iNEL5VT9mstz5hKAlcd+QR6H3aA==", + "license": "MIT", + "dependencies": { + "set-getter": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine/node_modules/get-value": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-1.3.1.tgz", + "integrity": "sha512-TrDxHI5wqgpM5Guhoz7xmblwy7kzhDauSs4df3NP907yFmLtCkOau8YtGo087jZXKDwP22NG6fCo0UA4EFLjOw==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "is-extendable": "^0.1.1", + "lazy-cache": "^0.2.4", + "noncharacters": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine/node_modules/kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine/node_modules/map-visit": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-0.1.5.tgz", + "integrity": "sha512-zdmJBFvvVR/H5wCfsCP7XxSLp+346yAZ30Wy2OsQLcH19OVGMWa3Ms9quO00lj9ybsySu3gKOINNgICb4Zqauw==", + "license": "MIT", + "dependencies": { + "lazy-cache": "^2.0.1", + "object-visit": "^0.3.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine/node_modules/map-visit/node_modules/lazy-cache": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", + "integrity": "sha512-7vp2Acd2+Kz4XkzxGxaB1FWOi8KjWIWsgdfD5MCb86DWvlLqhRPM+d6Pro3iNEL5VT9mstz5hKAlcd+QR6H3aA==", + "license": "MIT", + "dependencies": { + "set-getter": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine/node_modules/object-visit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-0.3.4.tgz", + "integrity": "sha512-6QNyX7uTuwqxP7pmDBqgBDKdmZws1rXriUyXM5KG6+7J0aYRuuAGoc636IGdLzgOL77WUwL+EpoTJrEHwWsyOA==", + "license": "MIT", + "dependencies": { + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine/node_modules/set-value": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.2.0.tgz", + "integrity": "sha512-dJaeu7V8d1KwjePimg1oOpGp31cEw/uRcZlfL7wwemkr+A00ev/ZhikvSMiQ4hkf83d8JdY2AFoFmXsKzmHMSw==", + "deprecated": "Critical bug fixed in v3.0.1, please upgrade to the latest version.", + "license": "MIT", + "dependencies": { + "isobject": "^1.0.0", + "noncharacters": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine/node_modules/set-value/node_modules/isobject": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-1.0.2.tgz", + "integrity": "sha512-WQQgFoML/sLgmhu9zTekYHZUJaPoa/fpVMQ8oxIuOvppzs70DxxyHZdAIjwcuuNDOVtNYsahhqtBbUvKwhRcGw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/error-symbol": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/error-symbol/-/error-symbol-0.1.0.tgz", + "integrity": "sha512-VyjaKxUmeDX/m2lxm/aknsJ1GWDWUO2Ze2Ad8S1Pb9dykAm9TjSKp5CjrNyltYqZ5W/PO6TInAmO2/BfwMyT1g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers/node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-next": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.14.tgz", + "integrity": "sha512-lmJ5F8ZgOYogq0qtH4L5SpxuASY2SPdOzqUprN2/56+P3GPsIpXaUWIJC66kYIH+yZdsM4nkHE5MIBP6s1NiBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "15.5.14", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^5.0.0" + }, + "peerDependencies": { + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.6", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha512-MsG3prOVw1WtLXAZbM3KiYtooKR1LvxHh3VHsVtIy0uiUu8usxgB/94DP2HxtD/661lLdB6yzQ09lGJSQr6nkg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-args": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/expand-args/-/expand-args-0.4.3.tgz", + "integrity": "sha512-bAAnw/WnKZUkA9PI3tk4oWRpyZkRiHtFSJ+W8dkTX/oXGhM3rz9Vo5+qW9sJ34z1da8jPap35/igXmE7lEjdsQ==", + "license": "MIT", + "dependencies": { + "expand-object": "^0.4.2", + "kind-of": "^3.0.3", + "lazy-cache": "^2.0.1", + "minimist": "^1.2.0", + "mixin-deep": "^1.1.3", + "omit-empty": "^0.4.1", + "set-value": "^0.3.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-args/node_modules/set-value": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.3.3.tgz", + "integrity": "sha512-aJPTd11HzK47w8xJMpyY4tBmFC6EidC8EG2fENxCJvPwLYzXLnNaesgo796y1fhSISSYAuah4Het+wDoPXK2tg==", + "deprecated": "Critical bug fixed in v3.0.1, please upgrade to the latest version.", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "isobject": "^2.0.0", + "to-object-path": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-args/node_modules/to-object-path": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.2.0.tgz", + "integrity": "sha512-6oMu4CTicplxUMOXBoS1W9YNjIclUzmWpWf02v+JnYMEGVX24rTCsYMHay85WA7Wq+9wZa2iJ+HAAX0yGOcxCQ==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "is-arguments": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha512-hxx03P2dJxss6ceIeri9cmYOT4SRs3Zk3afZwWpOsRqLqprhTR8u++SlC+sFGsQr7WGFPdMF7Gjc1njDLDK6UA==", + "license": "MIT", + "dependencies": { + "is-posix-bracket": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-object": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/expand-object/-/expand-object-0.4.2.tgz", + "integrity": "sha512-rC0h+knI3YE2rT9v2m6HIowp1aLAVo19u02/wRzE+Dl5eyPowLRcWVyLQ3UaIjSLvjfsTiE0xGb0qqrap5ABKw==", + "license": "MIT", + "dependencies": { + "get-stdin": "^5.0.1", + "is-number": "^2.1.0", + "minimist": "^1.2.0", + "set-value": "^0.3.3" + }, + "bin": { + "expand-object": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-object/node_modules/is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-object/node_modules/set-value": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.3.3.tgz", + "integrity": "sha512-aJPTd11HzK47w8xJMpyY4tBmFC6EidC8EG2fENxCJvPwLYzXLnNaesgo796y1fhSISSYAuah4Het+wDoPXK2tg==", + "deprecated": "Critical bug fixed in v3.0.1, please upgrade to the latest version.", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "isobject": "^2.0.0", + "to-object-path": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-object/node_modules/to-object-path": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.2.0.tgz", + "integrity": "sha512-6oMu4CTicplxUMOXBoS1W9YNjIclUzmWpWf02v+JnYMEGVX24rTCsYMHay85WA7Wq+9wZa2iJ+HAAX0yGOcxCQ==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "is-arguments": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-pkg": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/expand-pkg/-/expand-pkg-0.1.9.tgz", + "integrity": "sha512-Qqtqzx/e8tODrDr0H8HtO7+nftN0wH9bsk3948KpKBZLrc86Cm3/8mRKJmDfNSDWWcuKsilMmFlKPhYx5gHYuA==", + "license": "MIT", + "dependencies": { + "component-emitter": "^1.2.1", + "debug": "^2.4.1", + "defaults-deep": "^0.2.4", + "export-files": "^2.1.1", + "get-value": "^2.0.6", + "kind-of": "^3.1.0", + "lazy-cache": "^2.0.2", + "load-pkg": "^3.0.1", + "mixin-deep": "^1.1.3", + "normalize-pkg": "^0.3.20", + "omit-empty": "^0.4.1", + "parse-author": "^1.0.0", + "parse-git-config": "^1.1.1", + "repo-utils": "^0.3.7" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/expand-pkg/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/expand-pkg/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha512-AFASGfIlnIbkKPQwX1yHaDjFvh/1gyKJODme52V6IORh69uEYgZp0o9C+qsIGNVEiuuhQU0CSSl++Rlegg1qvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "license": "MIT", + "dependencies": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-tilde": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", + "integrity": "sha512-rtmc+cjLZqnu9dSYosX9EWmSJhTwpACgJQTfj4hgg2JjOD/6SIQalZrt4a3aQeh++oNxkazcaxrhPUj6+g5G/Q==", + "license": "MIT", + "dependencies": { + "os-homedir": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/export-files": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/export-files/-/export-files-2.1.1.tgz", + "integrity": "sha512-r2x1Zt0OKgdXRy0bXis3sOI8TNYmo5Fe71qXwsvpYaMvIlH5G0fWEf3AYiE2bONjePdSOojca7Jw+p9CQ6/6NQ==", + "license": "MIT", + "dependencies": { + "lazy-cache": "^1.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/export-files/node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha512-1FOj1LOwn42TMrruOHGt18HemVnbwAmAak7krWk+wa93KXxGbK+2jpezm+ytJYDaBX0/SPLZFHKM7m+tKobWGg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/falsey": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/falsey/-/falsey-0.3.2.tgz", + "integrity": "sha512-lxEuefF5MBIVDmE6XeqCdM4BWk1+vYmGZtkbKZ/VFcg6uBBw6fXNEbWmxCjDdQlFc9hy450nkiWwM3VAW6G1qg==", + "license": "MIT", + "dependencies": { + "kind-of": "^5.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/falsey/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-contents": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/file-contents/-/file-contents-0.2.4.tgz", + "integrity": "sha512-PEz7U6YlXr+dvWCtW63DUY1LUTHOVs1rv4s1/I/39dpvvidQqMSTY6JklazQS60MMoI/ztpo5kMlpdvGagvLbA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.0", + "file-stat": "^0.1.0", + "graceful-fs": "^4.1.2", + "is-buffer": "^1.1.0", + "is-utf8": "^0.2.0", + "lazy-cache": "^0.2.3", + "through2": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/file-contents/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-is-binary": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-is-binary/-/file-is-binary-1.0.0.tgz", + "integrity": "sha512-71I2LciuolZDBUCu4JzFBKxSvVurMD84G97uCYgt9PZ7ElhEomGqYHTKKU2NcDOxR1g2bwn+hRbkTFSrD80Pfw==", + "license": "MIT", + "dependencies": { + "is-binary-buffer": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/file-is-binary/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/file-name": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/file-name/-/file-name-0.1.0.tgz", + "integrity": "sha512-Q8SskhjF4eUk/xoQkmubwLkoHwOTv6Jj/WGtOVLKkZ0vvM+LipkSXugkn1F/+mjWXU32AXLZB3qaz0arUzgtRw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/file-stat": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/file-stat/-/file-stat-0.1.3.tgz", + "integrity": "sha512-f72m4132aOd5DVtREdDX8I0Dd7Zf/3PiUYYvn4BFCxfsLqj6r8joBZzrRlfvsNvxhADw+jpEa0AnWPII9H0Fbg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "lazy-cache": "^0.2.3", + "through2": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/file-stat/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha512-BTCqyBaWBTsauvnHiE8i562+EdJj+oUpkqWp2R1iCoR8f6oo8STRu3of7WJJ0TqWtxN50a5YFpzYK4Jj9esYfQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-file-up": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz", + "integrity": "sha512-mBxmNbVyjg1LQIIpgO8hN+ybWBgDQK8qjht+EbrTCGmmPV/sc7RF1i9stPTD6bpvXZywBdrwRYxhSdJv867L6A==", + "license": "MIT", + "dependencies": { + "fs-exists-sync": "^0.1.0", + "resolve-dir": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-pkg": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz", + "integrity": "sha512-0rnQWcFwZr7eO0513HahrWafsc3CTFioEB7DRiEYCUM/70QXSY8f3mCST17HXLcPvEhzH/Ty/Bxd72ZZsr/yvw==", + "license": "MIT", + "dependencies": { + "find-file-up": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/first-chunk-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", + "integrity": "sha512-ArRi5axuv66gEsyl3UuK80CzW7t56hem73YGNYxNWTGNKFJUadSb9Gu9SHijYEUi8ulQMf1bJomYNwSCPHhtTQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fs-exists-sync": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", + "integrity": "sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "license": "ISC" + }, + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stdin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", + "integrity": "sha512-jZV7n6jGE3Gt7fgSTJoz91Ak5MuTLwMwkoYdjxuJ/AmjIsE1UC03y/IWkZCQGEvVNS9qoRNwy5BCqxImv0FVeA==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/get-view": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/get-view/-/get-view-0.1.3.tgz", + "integrity": "sha512-PZOmJnoY9wEDzAWW/0L6vRVfmPx/iKNiAxXdEI83dD8EPaqnI3GQraUTTSVgIVt5R1ja25/C3ARQAyVSkxN2Cg==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.0", + "match-file": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/get-view/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/git-config-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/git-config-path/-/git-config-path-1.0.1.tgz", + "integrity": "sha512-KcJ2dlrrP5DbBnYIZ2nlikALfRhKzNSX0stvv3ImJ+fvC4hXKoV+U+74SV0upg+jlQZbrtQzc0bu6/Zh+7aQbg==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "fs-exists-sync": "^0.1.0", + "homedir-polyfill": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/git-repo-name": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/git-repo-name/-/git-repo-name-0.6.0.tgz", + "integrity": "sha512-DF4XxB6H+Te79JA08/QF/IjIv+j+0gF990WlgAX3SXXU2irfqvBc/xxlAIh6eJWYaKz45MrrGVBFS0Qc4bBz5g==", + "license": "MIT", + "dependencies": { + "cwd": "^0.9.1", + "file-name": "^0.1.0", + "lazy-cache": "^1.0.4", + "remote-origin-url": "^0.5.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/git-repo-name/node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha512-ab1S1g1EbO7YzauaJLkgLp7DZVAqj9M/dvKlTt8DkXA2tiOIcSMrlVI2J1RZyB5iJVccEscjGn+kpOG9788MHA==", + "license": "MIT", + "dependencies": { + "glob-parent": "^2.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-base/node_modules/glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha512-JDYOvfxio/t42HKdxkAYaCiBN7oYiuxykOxKxdaUW5Qn0zaYN3gRQWolrwdnf0shM9/EP0ebuuTmyoXNr1cC5w==", + "license": "ISC", + "dependencies": { + "is-glob": "^2.0.0" + } + }, + "node_modules/glob-base/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-base/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-stream": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz", + "integrity": "sha512-piN8XVAO2sNxwVLokL4PswgJvK/uQ6+awwXUVRTGF+rRfgCZpn4hOqxiRuTEbU/k3qgKl0DACYQ/0Sge54UMQg==", + "license": "MIT", + "dependencies": { + "extend": "^3.0.0", + "glob": "^5.0.3", + "glob-parent": "^3.0.0", + "micromatch": "^2.3.7", + "ordered-read-streams": "^0.3.0", + "through2": "^0.6.0", + "to-absolute-glob": "^0.1.1", + "unique-stream": "^2.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/glob-stream/node_modules/braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha512-xU7bpz2ytJl1bH9cgIurjpg/n8Gohy9GTw81heDYLJQ4RU60dlyJsa+atVF2pI0yMMvKxI9HkKwjePCj5XI1hw==", + "license": "MIT", + "dependencies": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-stream/node_modules/glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/glob-stream/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "license": "ISC", + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/glob-stream/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-stream/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/glob-stream/node_modules/micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha512-LnU2XFEk9xxSJ6rfgAry/ty5qwUTyHYOBU0g4R6tIw5ljwgGIBmiKhRWLw5NpMOnrgUNcDJ4WMp8rl3sYVHLNA==", + "license": "MIT", + "dependencies": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-stream/node_modules/micromatch/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-stream/node_modules/micromatch/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-stream/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-stream/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/glob-stream/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, + "node_modules/glob-stream/node_modules/through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==", + "license": "MIT", + "dependencies": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "node_modules/global-modules": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", + "integrity": "sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA==", + "license": "MIT", + "dependencies": { + "global-prefix": "^0.1.4", + "is-windows": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", + "integrity": "sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw==", + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.0", + "ini": "^1.3.4", + "is-windows": "^0.2.0", + "which": "^1.2.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/gray-matter": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-3.1.1.tgz", + "integrity": "sha512-nZ1qjLmayEv0/wt3sHig7I0s3/sJO0dkAaKYQ5YAOApUtYEOonXSFdWvL1khvnZMTvov4UufkqlFsilPnejEXA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "js-yaml": "^3.10.0", + "kind-of": "^5.0.2", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/gray-matter/node_modules/kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/group-array": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/group-array/-/group-array-0.3.4.tgz", + "integrity": "sha512-YAmNsgsi1uQ7Ai3T4FFkMoskqbLEUPRajAmrn8FclwZQQnV98NLrNWjQ3n2+i1pANxdO3n6wsNEkKq5XrYy0Ow==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "for-own": "^0.1.4", + "get-value": "^2.0.6", + "kind-of": "^3.1.0", + "split-string": "^1.0.1", + "union-value": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/group-array/node_modules/split-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-1.0.1.tgz", + "integrity": "sha512-ZuVODgxrpJnBD5LezfE484E2ArRF8HGgJqaiGBWvCbGS1iqynO45FQxBx7Ze4t45X9a994ejFD5kLhI6WtL1xA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-choose-files": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/gulp-choose-files/-/gulp-choose-files-0.1.3.tgz", + "integrity": "sha512-SuAg0I2iCMEDcE3BJ46cfIo1Gn5N16403eie6G/iqrttDuKJUK1q3wh/2HBP/ZAJAqNXABI0uEavL2QxSMka1A==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "question-cache": "^0.5.1", + "through2": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-choose-files/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/gulp-choose-files/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/gulp-choose-files/node_modules/question-cache": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/question-cache/-/question-cache-0.5.1.tgz", + "integrity": "sha512-v9F1LnlSQIUEAGFtrfVX/76lH4u4zyV34t94o6EkguPTKKfbvV6SLH8h3pn7LXGZLmAgD1PbmVOuKMY8ZWnuPg==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "arr-union": "^3.1.0", + "async-each-series": "^1.1.0", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "get-value": "^2.0.6", + "has-value": "^0.3.1", + "inquirer2": "^0.1.1", + "is-answer": "^0.1.0", + "isobject": "^2.1.0", + "lazy-cache": "^2.0.1", + "mixin-deep": "^1.1.3", + "omit-empty": "^0.4.1", + "option-cache": "^3.4.0", + "os-homedir": "^1.0.1", + "project-name": "^0.2.5", + "set-value": "^0.3.3", + "to-choices": "^0.2.0", + "use": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-choose-files/node_modules/set-value": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.3.3.tgz", + "integrity": "sha512-aJPTd11HzK47w8xJMpyY4tBmFC6EidC8EG2fENxCJvPwLYzXLnNaesgo796y1fhSISSYAuah4Het+wDoPXK2tg==", + "deprecated": "Critical bug fixed in v3.0.1, please upgrade to the latest version.", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "isobject": "^2.0.0", + "to-object-path": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-choose-files/node_modules/to-object-path": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.2.0.tgz", + "integrity": "sha512-6oMu4CTicplxUMOXBoS1W9YNjIclUzmWpWf02v+JnYMEGVX24rTCsYMHay85WA7Wq+9wZa2iJ+HAAX0yGOcxCQ==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "is-arguments": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-choose-files/node_modules/use": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/use/-/use-2.0.2.tgz", + "integrity": "sha512-RrhWfFWkNCz3djfSFZh7uSwu491QRhwNaHyAgB2sGl4kmmznb5ZUuuHpiWLVEsXOdpDakYK/x5+9o4lgg41UMw==", + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "lazy-cache": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-choose-files/node_modules/use/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-sourcemaps": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz", + "integrity": "sha512-NjRy6+Qb5K1xbwOvPviD3uA4KSq2zsalPL+4vxPQPuL+kKzHjXJL10/kLaESic3LmBto8VIBHr3gIN3F9AjnhA==", + "license": "ISC", + "dependencies": { + "convert-source-map": "^1.1.1", + "graceful-fs": "^4.1.2", + "strip-bom": "^2.0.0", + "through2": "^2.0.0", + "vinyl": "^1.0.0" + } + }, + "node_modules/gulp-sourcemaps/node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "license": "MIT", + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-glob": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/has-glob/-/has-glob-0.1.1.tgz", + "integrity": "sha512-WMHzb7oCwDcMDngWy0b+viLjED8zvSi5d4/YdBetADHX/rLH+noJaRTytuyN6thTxxM7lK+FloogQHHdOOR+7g==", + "license": "MIT", + "dependencies": { + "is-glob": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-glob/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-glob/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-own-deep": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-own-deep/-/has-own-deep-0.1.4.tgz", + "integrity": "sha512-a9Dn8Q46DZySlvZqjCX5rkwS9AYIv3VQM3IoOhTXJVJ/cEmVDMLTrJClIihLS0a09PzhrEBbueji44ZQjLh19g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "license": "MIT", + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helper-cache": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/helper-cache/-/helper-cache-0.7.2.tgz", + "integrity": "sha512-ictXA4Nsj9HZcY5Sf4PyWKOXRkQLCDLJLvekaKKrQ+IGLMe4Z+u2oM1QqRGjtWeQRfQCA3NJyIzZpfmw6GvwOQ==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "lazy-cache": "^0.2.3", + "lodash.bind": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/helper-cache/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/info-symbol": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/info-symbol/-/info-symbol-0.1.0.tgz", + "integrity": "sha512-qkc9wjLDQ+dYYZnY5uJXGNNHyZ0UOMDUnhvy0SEZGVVYmQ5s4i8cPAin2MbU6OxJgi8dfj/AnwqPx0CJE6+Lsw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/inquirer2": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/inquirer2/-/inquirer2-0.1.1.tgz", + "integrity": "sha512-U7R6xvJmmcAx8Bq3Ok7+9L5kyBiUbCokZJMSibn+lDQasL9RtW9kYmnO5fezF0EcqE+pt4Hp3gc5XBGCqLkRDg==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^1.1.1", + "ansi-regex": "^2.0.0", + "arr-flatten": "^1.0.1", + "arr-pluck": "^0.1.0", + "array-unique": "^0.2.1", + "chalk": "^1.1.1", + "cli-cursor": "^1.0.2", + "cli-width": "^1.1.0", + "extend-shallow": "^2.0.1", + "figures": "^1.4.0", + "is-number": "^2.1.0", + "is-plain-object": "^2.0.1", + "lazy-cache": "^1.0.3", + "lodash.where": "^3.1.0", + "readline2": "^1.0.1", + "run-async": "^0.1.0", + "rx-lite": "^4.0.7", + "strip-color": "^0.1.0", + "through2": "^2.0.0" + } + }, + "node_modules/inquirer2/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inquirer2/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inquirer2/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inquirer2/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/inquirer2/node_modules/is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inquirer2/node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inquirer2/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inquirer2/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-absolute": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.2.6.tgz", + "integrity": "sha512-7Kr05z5LkcOpoMvxHN1PC11WbPabdNFmMYYo0eZvWu3BfVS0T03yoqYDczoCBx17xqk2x1XAZrcKiFVL88jxlQ==", + "license": "MIT", + "dependencies": { + "is-relative": "^0.2.1", + "is-windows": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", + "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-answer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-answer/-/is-answer-0.1.1.tgz", + "integrity": "sha512-ifVYWfVjXzeNx32XK7twC8xMzVYfOqFGETEuwww/Oo8OZQe/tv+huAjP+05qP8omK+IfLmPWN0omZ7YvIvejMw==", + "license": "MIT", + "dependencies": { + "has-values": "^0.1.4", + "is-primitive": "^2.0.0", + "omit-empty": "^0.4.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-binary-buffer/-/is-binary-buffer-1.0.0.tgz", + "integrity": "sha512-fP08vt1YuBWSWdDCWkHUDo/Gb+YpnsiK41w2kP3iAkWhMKV4uuAAwPQm9GkA4r+OCDzpa+APIOaHZW6d83e5Ug==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha512-9YclgOGtN/f8zx0Pr4FQYMdibBiTaH3sn52vjYip4ZSf6C4/6RfTEZ+MR4GvKhCxdPh21Bg42/WL55f6KSnKpg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha512-0EygVC5qPvIyb+gSz7zdD5/AAoS6Qrx1e//6N4yv4oNm30kqvdmG66oZFWVlQHUWe5OjP08FuTw2IdT0EOTcYA==", + "license": "MIT", + "dependencies": { + "is-primitive": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-generator/-/is-generator-1.0.3.tgz", + "integrity": "sha512-G56jBpbJeg7ds83HW1LuShNs8J73Fv3CPz/bmROHOHlnKkN8sWb9ujiagjmxxMUywftgq48HlBZELKKqFLk0oA==", + "license": "MIT" + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha512-Yu68oeXJ7LeWNmZ3Zov/xg/oDBnBK2RNxwYY1ilNJX+tKKZqgPK+qOn/Gs9jEu66KDY9Netf5XLKNGzas/vPfQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha512-N3w1tFaRfk3UrPfqeRyD+GYDASU3W5VinKhlORy8EWVf/sIdDL9GAcew85XmktCfH+ngG7SRXEVDoO18WMdB/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-registered": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/is-registered/-/is-registered-0.1.5.tgz", + "integrity": "sha512-dOOjAYNmKGtjoW229wn/SDmrO65oQcUvng9WUYF/AIZAQZG/l+puNUPt+/x7YCn4W9A33H6LItHgSETDmS0urg==", + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "isobject": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-relative": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.2.1.tgz", + "integrity": "sha512-9AMzjRmLqcue629b4ezEVSK6kJsYJlUIhMcygmYORUgwUNJiavHcC3HkaGx0XYpyVKQSOqFbMEZmW42cY87sYw==", + "license": "MIT", + "dependencies": { + "is-unc-path": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unc-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-0.1.2.tgz", + "integrity": "sha512-HhLc5VDMH4pu3oMtIuunz/DFQUIoR561kMME3U3Afhj8b7vH085vkIkemrz1kLXCEIuoMAmO3yVmafWdSbGW8w==", + "license": "MIT", + "dependencies": { + "unc-path-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "license": "MIT" + }, + "node_modules/is-valid-app": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-valid-app/-/is-valid-app-0.2.1.tgz", + "integrity": "sha512-2/qNSVFKyi5WiaIgv153Vt2ZM7T7HSlUu/m3HMnoyp6pk5NYhOUz0aU7Gx2DGYRnZ/8q+pMOwd93pCE8uWhvBg==", + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "is-registered": "^0.1.5", + "is-valid-instance": "^0.2.0", + "lazy-cache": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-valid-app/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/is-valid-app/node_modules/is-valid-instance": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-valid-instance/-/is-valid-instance-0.2.0.tgz", + "integrity": "sha512-dNT7bamkigo07gvbnoBRABSNX1ayAhkcw6/3fYhVDhiPXiqnCouD4JMmrozyOx37UUlC+Se1j/jCfLo1fNs0Ng==", + "license": "MIT", + "dependencies": { + "isobject": "^2.1.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-valid-app/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/is-valid-glob": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz", + "integrity": "sha512-CvG8EtJZ8FyzVOGPzrDorzyN65W1Ld8BVnqshRCah6pFIsprGx3dKgFtjLn/Vw9kGqR4OlR84U7yhT9ZVTyWIQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-valid-instance": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-valid-instance/-/is-valid-instance-0.1.0.tgz", + "integrity": "sha512-js5DRu650+u3zcGfCe23npdFtPuBeLx3iR8q2vfCO4m1KqNz5R35fDQlLPm++gAzg5H+OJXDOG5LGyn8pzl/1Q==", + "license": "MIT", + "dependencies": { + "isobject": "^2.1.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-whitespace": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-whitespace/-/is-whitespace-0.3.0.tgz", + "integrity": "sha512-RydPhl4S6JwAyj0JJjshWJEFG6hNye3pZFBRZaTUfZFwGHxzppNaNOVgQuS/E/SlhrApuMXrpnK1EEIXfdo3Dg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "license": "MIT", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isobject/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/layouts": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/layouts/-/layouts-0.11.0.tgz", + "integrity": "sha512-Zt65tua9otUMsfoQMAKmUSMGBwgkchSCc33ko/xBBSGnc/Q4+G8gJgouynZy7/iSnzpt3+myRRDQ9HQ5cctSog==", + "license": "MIT", + "dependencies": { + "delimiter-regex": "^1.3.1", + "falsey": "^0.3.0", + "get-view": "^0.1.1", + "lazy-cache": "^1.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/layouts/node_modules/delimiter-regex": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/delimiter-regex/-/delimiter-regex-1.3.1.tgz", + "integrity": "sha512-NyEdbzFCa0imbFMxQH6X5AB/DxngubpAAiQEqaam+YYcT0gGiM1gFo410HwpiPOruHl8HfFM913tFLjA8kkvHg==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/layouts/node_modules/extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", + "license": "MIT", + "dependencies": { + "kind-of": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/layouts/node_modules/kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/layouts/node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lazy-cache": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", + "integrity": "sha512-7vp2Acd2+Kz4XkzxGxaB1FWOi8KjWIWsgdfD5MCb86DWvlLqhRPM+d6Pro3iNEL5VT9mstz5hKAlcd+QR6H3aA==", + "license": "MIT", + "dependencies": { + "set-getter": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/load-helpers": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/load-helpers/-/load-helpers-0.2.11.tgz", + "integrity": "sha512-+iUnxQSddtpXoeRrza02jbJOUgCbJGG6GGeE4WTf6nV0Z0uR+/+/h2RMfDAl5SI4Cd/fu5xFPqo0ibP3v9y1ew==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-valid-glob": "^0.3.0", + "lazy-cache": "^2.0.1", + "matched": "^0.4.1", + "resolve-dir": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-pkg": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/load-pkg/-/load-pkg-3.0.1.tgz", + "integrity": "sha512-wW6PBOWKbPceeIamjHjoacmI0F7Q+JdHoYl1nYE3lGOQCmq+xAnfIp24dqhUSfsO6Y7YSlrmyi3JxvSiRnoivg==", + "license": "MIT", + "dependencies": { + "find-pkg": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-templates": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/load-templates/-/load-templates-0.11.4.tgz", + "integrity": "sha512-roLgv19smhcE2x9mBvuuUzj3u3jRL+lWr+7u6v0KSk2wtdX0v8KOEHYZGBUdMjY1YPIh9864YQdO0SqpxiA+6Q==", + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "glob-parent": "^2.0.0", + "has-glob": "^0.1.1", + "is-valid-glob": "^0.3.0", + "lazy-cache": "^2.0.1", + "matched": "^0.4.1", + "to-file": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-templates/node_modules/glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha512-JDYOvfxio/t42HKdxkAYaCiBN7oYiuxykOxKxdaUW5Qn0zaYN3gRQWolrwdnf0shM9/EP0ebuuTmyoXNr1cC5w==", + "license": "ISC", + "dependencies": { + "is-glob": "^2.0.0" + } + }, + "node_modules/load-templates/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-templates/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash._arrayfilter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._arrayfilter/-/lodash._arrayfilter-3.0.0.tgz", + "integrity": "sha512-xi4jscMHMkWtF8vXNpmvAXTmes6gKMpXsWM8kKuJ5tfk/VhJujrAG2sVc/LBsUERkReV9blMG2GD4SjPHyqaTw==", + "license": "MIT" + }, + "node_modules/lodash._basecallback": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/lodash._basecallback/-/lodash._basecallback-3.3.1.tgz", + "integrity": "sha512-LQffghuO63ufDY33KKO1ezGKbcFZK3ngYV7JpxaUomoM5acf0YeXU3Pm8csVE0girVs50TXzfNibl69Co3ggJA==", + "license": "MIT", + "dependencies": { + "lodash._baseisequal": "^3.0.0", + "lodash._bindcallback": "^3.0.0", + "lodash.isarray": "^3.0.0", + "lodash.pairs": "^3.0.0" + } + }, + "node_modules/lodash._baseeach": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash._baseeach/-/lodash._baseeach-3.0.4.tgz", + "integrity": "sha512-IqUZ9MQo2UT1XPGuBntInqTOlc+oV+bCo0kMp+yuKGsfvRSNgUW0YjWVZUrG/gs+8z/Eyuc0jkJjOBESt9BXxg==", + "license": "MIT", + "dependencies": { + "lodash.keys": "^3.0.0" + } + }, + "node_modules/lodash._basefilter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._basefilter/-/lodash._basefilter-3.0.0.tgz", + "integrity": "sha512-EjWjqBE5KHmvrzgZ9tSvt7ggGmDF0pjPzaiUONQ97M4+YDYW8VMH3VnyKS/JHFoqDAYEIIx+3/Tg4C0zlC6qPA==", + "license": "MIT", + "dependencies": { + "lodash._baseeach": "^3.0.0" + } + }, + "node_modules/lodash._baseisequal": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz", + "integrity": "sha512-U+3GsNEZj9ebI03ncLC2pLmYVjgtYZEwdkAPO7UGgtGvAz36JVFPAQUufpSaVL93Cz5arc6JGRKZRhaOhyVJYA==", + "license": "MIT", + "dependencies": { + "lodash.isarray": "^3.0.0", + "lodash.istypedarray": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "node_modules/lodash._baseismatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lodash._baseismatch/-/lodash._baseismatch-3.1.3.tgz", + "integrity": "sha512-lq0Z+O/HfAJ16frtiZnvi2sLQrFfcYxK2q5R+n10+cWbXQ/Mz6R52mLOX/8R3npLGIO7Rq7zNP7ENTCJB/GN+g==", + "license": "MIT", + "dependencies": { + "lodash._baseisequal": "^3.0.0" + } + }, + "node_modules/lodash._basematches": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._basematches/-/lodash._basematches-3.2.0.tgz", + "integrity": "sha512-E6aibw9mFnfTO8z4zu1Fc2Pgv102/c11RtunY0MBdnIRWy27CtwnTVBQjfXohtUoDH1BI+vxZ9+b2JJY13dt3A==", + "license": "MIT", + "dependencies": { + "lodash._baseismatch": "^3.0.0", + "lodash.pairs": "^3.0.0" + } + }, + "node_modules/lodash._bindcallback": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", + "integrity": "sha512-2wlI0JRAGX8WEf4Gm1p/mv/SZ+jLijpj0jyaE/AXeuQphzCgD8ZQW4oSpoN8JAopujOFGU3KMuq7qfHBWlGpjQ==", + "license": "MIT" + }, + "node_modules/lodash._createwrapper": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._createwrapper/-/lodash._createwrapper-3.2.0.tgz", + "integrity": "sha512-O8fi7P57KZQjtTJN3tbUAJsm6Coo35JVi4OiEU/WV0rrqaWemk+rRB/1ohiIiv1cIK3dIkVhMehaFOFyNZDYkQ==", + "license": "MIT", + "dependencies": { + "lodash._root": "^3.0.0" + } + }, + "node_modules/lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==", + "license": "MIT" + }, + "node_modules/lodash._replaceholders": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._replaceholders/-/lodash._replaceholders-3.0.0.tgz", + "integrity": "sha512-FbnZp+6+UaT8VzGNXUK8nIH7rC/P+c2te5R/rpjgwLY27OsEMqCyF6yOxqHMj9Qv3yelSVVuYzCjtrJzcKbAhg==", + "license": "MIT" + }, + "node_modules/lodash._root": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", + "integrity": "sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==", + "license": "MIT" + }, + "node_modules/lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw==", + "license": "MIT" + }, + "node_modules/lodash.bind": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-3.1.0.tgz", + "integrity": "sha512-GaXlyWuJbyuJ54vRypYLVq1NS4v7QIBVicEX4lmW8PE5XaltCuFzWLG4WuXKYQ7SKfzxkiEsadQyuVOxym7paQ==", + "license": "MIT", + "dependencies": { + "lodash._createwrapper": "^3.0.0", + "lodash._replaceholders": "^3.0.0", + "lodash.restparam": "^3.0.0" + } + }, + "node_modules/lodash.filter": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz", + "integrity": "sha512-pXYUy7PR8BCLwX5mgJ/aNtyOvuJTdZAo9EQFUvMIYugqmJxnrYaANvTbgndOzHSCSR0wnlBBfRXJL5SbWxo3FQ==", + "license": "MIT" + }, + "node_modules/lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", + "license": "MIT" + }, + "node_modules/lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==", + "license": "MIT" + }, + "node_modules/lodash.initial": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.initial/-/lodash.initial-4.1.1.tgz", + "integrity": "sha512-/eZXy8y0IGQTuCKScq32mU+O/Qc160EfYPrAD7y4oXPAgWdQvyxxhTOIpl+tDfP86yT7jrMtUA8noSqYUdKWQg==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha512-JwObCrNJuT0Nnbuecmqr5DgtuBppuCvGD9lxjFpAzwnVtdGoDQ1zig+5W8k5/6Gcn0gZ3936HDAlGd28i7sOGQ==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.istypedarray": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz", + "integrity": "sha512-lGWJ6N8AA3KSv+ZZxlTdn4f6A7kMfpJboeyvbFdE7IU9YAgweODqmOgdUHOA+c6lVWeVLysdaxciFXi+foVsWw==", + "license": "MIT" + }, + "node_modules/lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha512-CuBsapFjcubOGMn3VD+24HOAPxM79tH+V6ivJL3CHYjtrawauDJHUk//Yew9Hvc6e9rbCrURGk8z6PC+8WJBfQ==", + "license": "MIT", + "dependencies": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "node_modules/lodash.last": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash.last/-/lodash.last-3.0.0.tgz", + "integrity": "sha512-14mq7rSkCxG4XMy9lF2FbIOqqgF0aH0NfPuQ3LPR3vIh0kHnUvIYP70dqa1Hf47zyXfQ8FzAg0MYOQeSuE1R7A==", + "license": "MIT" + }, + "node_modules/lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.pairs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash.pairs/-/lodash.pairs-3.0.1.tgz", + "integrity": "sha512-lgXvpU43ZNQrZ/pK2cR97YzKeAno3e3HhcyvLKsofljeHKrQcZhT1vW7fg4X61c92tM+mjD/DypoLZYuAKNIkQ==", + "license": "MIT", + "dependencies": { + "lodash.keys": "^3.0.0" + } + }, + "node_modules/lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha512-L4/arjjuq4noiUJpt3yS6KIKDtJwNe2fIYgMqyYYKoeIfV1iEqvPwhCx23o+R9dzouGihDAPN1dTIRWa7zk8tw==", + "license": "MIT" + }, + "node_modules/lodash.where": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.where/-/lodash.where-3.1.0.tgz", + "integrity": "sha512-9iH6No94IEtewjRRAykRVVW4Sw0DULKFp9H7x92MvbYUjg5EHj/+o58/Jx/kxAu7UWJLItwBH4FemHaQIGFIeg==", + "license": "MIT", + "dependencies": { + "lodash._arrayfilter": "^3.0.0", + "lodash._basecallback": "^3.0.0", + "lodash._basefilter": "^3.0.0", + "lodash._basematches": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "node_modules/log-ok": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/log-ok/-/log-ok-0.1.1.tgz", + "integrity": "sha512-cc8VrkS6C+9TFuYAwuHpshrcrGRAv7d0tUJ0GdM72ZBlKXtlgjUZF84O+OhQUdiVHoF7U/nVxwpjOdwUJ8d3Vg==", + "license": "MIT", + "dependencies": { + "ansi-green": "^0.1.1", + "success-symbol": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/log-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/log-utils/-/log-utils-0.2.1.tgz", + "integrity": "sha512-udyegKoMz9eGfpKAX//Khy7sVAZ8b1F7oLDnepZv/1/y8xTvsyPgqQrM94eG8V0vcc2BieYI2kVW4+aa6m+8Qw==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^0.2.0", + "error-symbol": "^0.1.0", + "info-symbol": "^0.1.0", + "log-ok": "^0.1.1", + "success-symbol": "^0.1.0", + "time-stamp": "^1.0.1", + "warning-symbol": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==", + "license": "MIT" + }, + "node_modules/lucide-react": { + "version": "0.323.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.323.0.tgz", + "integrity": "sha512-rTXZFILl2Y4d1SG9p1Mdcf17AcPvPvpc/egFIzUrp7IUy60MUQo3Oi1mu8LGYXUVwuRZYsSMt3csHRW5mAovJg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/make-iterator/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-config": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/map-config/-/map-config-0.5.0.tgz", + "integrity": "sha512-7pgduXtyOXZ/py4n6IM8G+7wanqbRDPK5Myp7P3jUUAFQwzGDeuMm0N8Dxrwaf3bySqJpne4NdglRUxdw7I7QQ==", + "license": "MIT", + "dependencies": { + "array-unique": "^0.2.1", + "async": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-schema": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/map-schema/-/map-schema-0.2.4.tgz", + "integrity": "sha512-1sgduImleUF+8NiS1wlqDJ8uhmJtFbLRjVW3PZP5IZJd1n+11eV91AnHI4jOYT2UCirriivNUgh6DG73V+G9QQ==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "collection-visit": "^0.2.3", + "component-emitter": "^1.2.1", + "debug": "^2.6.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "get-value": "^2.0.6", + "is-primitive": "^2.0.0", + "kind-of": "^3.1.0", + "lazy-cache": "^2.0.2", + "log-utils": "^0.2.1", + "longest": "^1.0.1", + "mixin-deep": "^1.1.3", + "object.omit": "^2.0.1", + "object.pick": "^1.2.0", + "omit-empty": "^0.4.1", + "pad-right": "^0.2.2", + "set-value": "^0.4.0", + "sort-object-arrays": "^0.1.1", + "union-value": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-schema/node_modules/collection-visit": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-0.2.3.tgz", + "integrity": "sha512-V88PJOCqJfsZS45YBELDgmhQkECokQAAr9XR4hT6eFkFsAPsCsk3EoDHSuBPYzygjquGM/0KF4vdwTiQO6lbdw==", + "license": "MIT", + "dependencies": { + "lazy-cache": "^2.0.1", + "map-visit": "^0.1.5", + "object-visit": "^0.3.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-schema/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/map-schema/node_modules/map-visit": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-0.1.5.tgz", + "integrity": "sha512-zdmJBFvvVR/H5wCfsCP7XxSLp+346yAZ30Wy2OsQLcH19OVGMWa3Ms9quO00lj9ybsySu3gKOINNgICb4Zqauw==", + "license": "MIT", + "dependencies": { + "lazy-cache": "^2.0.1", + "object-visit": "^0.3.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-schema/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/map-schema/node_modules/object-visit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-0.3.4.tgz", + "integrity": "sha512-6QNyX7uTuwqxP7pmDBqgBDKdmZws1rXriUyXM5KG6+7J0aYRuuAGoc636IGdLzgOL77WUwL+EpoTJrEHwWsyOA==", + "license": "MIT", + "dependencies": { + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-schema/node_modules/set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha512-2Z0LRUUvYeF7gIFFep48ksPq0NR09e5oKoFXznaMGNcu+EZAfGnyL0K6xno2gCqX6dZYEZRjrcn04/gvZzcKhQ==", + "deprecated": "Critical bug fixed in v3.0.1, please upgrade to the latest version.", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-schema/node_modules/union-value": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-0.2.4.tgz", + "integrity": "sha512-Tv3cqdyY8yjW9ZcJ9WP7JdHS34natzylD0oNRLlYbWOfUdC4EQ0sf3fubnqrK2IErtlmobFmuS1pWvv88VghpA==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", + "license": "MIT", + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/match-file": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/match-file/-/match-file-0.2.2.tgz", + "integrity": "sha512-BDEZIcrBSnooL0zC72Yt3z1HhJiCq+2pMnHKVDeYN/cilCrz3KrpqKPm4ZOfWCoDolRl4QyKQpfRlQWF6PqnjQ==", + "license": "MIT", + "dependencies": { + "is-glob": "^3.1.0", + "isobject": "^3.0.0", + "micromatch": "^2.3.11" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/match-file/node_modules/braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha512-xU7bpz2ytJl1bH9cgIurjpg/n8Gohy9GTw81heDYLJQ4RU60dlyJsa+atVF2pI0yMMvKxI9HkKwjePCj5XI1hw==", + "license": "MIT", + "dependencies": { + "expand-range": "^1.8.1", + "preserve": "^0.2.0", + "repeat-element": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/match-file/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/match-file/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/match-file/node_modules/micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha512-LnU2XFEk9xxSJ6rfgAry/ty5qwUTyHYOBU0g4R6tIw5ljwgGIBmiKhRWLw5NpMOnrgUNcDJ4WMp8rl3sYVHLNA==", + "license": "MIT", + "dependencies": { + "arr-diff": "^2.0.0", + "array-unique": "^0.2.1", + "braces": "^1.8.2", + "expand-brackets": "^0.1.4", + "extglob": "^0.3.1", + "filename-regex": "^2.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.1", + "kind-of": "^3.0.2", + "normalize-path": "^2.0.1", + "object.omit": "^2.0.0", + "parse-glob": "^3.0.4", + "regex-cache": "^0.4.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/match-file/node_modules/micromatch/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/match-file/node_modules/micromatch/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/match-file/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "license": "MIT", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/matched": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/matched/-/matched-0.4.4.tgz", + "integrity": "sha512-zpasnbB5vQkvb0nfcKV0zEoGgMtV7atlWR1Vk3E8tEKh6EicMseKtVV+5vc+zsZwvDlcNMKlKK/CVOEeAalYRQ==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "async-array-reduce": "^0.2.0", + "extend-shallow": "^2.0.1", + "fs-exists-sync": "^0.1.0", + "glob": "^7.0.5", + "has-glob": "^0.1.1", + "is-valid-glob": "^0.3.0", + "lazy-cache": "^2.0.1", + "resolve-dir": "^0.1.0" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/matched/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/math-random": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", + "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", + "license": "MIT" + }, + "node_modules/merge-deep": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", + "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-stream": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-0.1.8.tgz", + "integrity": "sha512-ivGsLZth/AkvevAzPlRLSie8Q3GdyH/5xUYgn+ItAJYslT0NsKd2cxx0bAjmqoY5swX0NoWJjvkDkfpaVZx9lw==", + "license": "MIT", + "dependencies": { + "through2": "^0.6.1" + } + }, + "node_modules/merge-stream/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/merge-stream/node_modules/readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/merge-stream/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "license": "MIT" + }, + "node_modules/merge-stream/node_modules/through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==", + "license": "MIT", + "dependencies": { + "readable-stream": ">=1.0.33-1 <1.1.0-0", + "xtend": ">=4.0.0 <4.1.0-0" + } + }, + "node_modules/merge-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/merge-value/-/merge-value-1.0.0.tgz", + "integrity": "sha512-fJMmvat4NeKz63Uv9iHWcPDjCWcCkoiRoajRTEO8hlhUC6rwaHg0QCF9hBOTjZmm4JuglPckPSTtcuJL5kp0TQ==", + "license": "MIT", + "dependencies": { + "get-value": "^2.0.6", + "is-extendable": "^1.0.0", + "mixin-deep": "^1.2.0", + "set-value": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-value/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "license": "MIT", + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-deep/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==", + "license": "MIT", + "dependencies": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object/node_modules/for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", + "integrity": "sha512-EbrziT4s8cWPmzr47eYVW3wimS4HsvlnV5ri1xw1aR6JQo/OrJX5rkl32K/QQHdxeabJETtfeaROGhd8W7uBgg==", + "license": "ISC" + }, + "node_modules/mz": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nanoseconds": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/nanoseconds/-/nanoseconds-0.1.0.tgz", + "integrity": "sha512-6yOHqTvJNI9xGmVHWQ4ZTYhGpT0O4h9N+uk/UuRVPI8TskViB4s4QL3y+jY/Yxsdz7gvoBGPCHWRUibOyyYMwA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/next": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.14.tgz", + "integrity": "sha512-M6S+4JyRjmKic2Ssm7jHUPkE6YUJ6lv4507jprsSZLulubz0ihO2E+S4zmQK3JZ2ov81JrugukKU4Tz0ivgqqQ==", + "license": "MIT", + "dependencies": { + "@next/env": "15.5.14", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.14", + "@next/swc-darwin-x64": "15.5.14", + "@next/swc-linux-arm64-gnu": "15.5.14", + "@next/swc-linux-arm64-musl": "15.5.14", + "@next/swc-linux-x64-gnu": "15.5.14", + "@next/swc-linux-x64-musl": "15.5.14", + "@next/swc-win32-arm64-msvc": "15.5.14", + "@next/swc-win32-x64-msvc": "15.5.14", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next-tick": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-0.2.2.tgz", + "integrity": "sha512-f7h4svPtl+QidoBv4taKXUjJ70G2asaZ8G28nS0OkqaalX8dwwrtWtyxEDPK62AC00ur/+/E0pUwBwY5EPn15Q==", + "license": "MIT" + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "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" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "license": "MIT", + "dependencies": { + "lower-case": "^1.1.1" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "dev": true, + "license": "MIT" + }, + "node_modules/noncharacters": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/noncharacters/-/noncharacters-1.1.0.tgz", + "integrity": "sha512-U69XzMNq7UQXR27xT17tkQsHPsLc+5W9yfXvYzVCwFxghVf+7VttxFnCKFMxM/cHD+/QIyU009263hxIIurj4g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-pkg": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/normalize-pkg/-/normalize-pkg-0.3.20.tgz", + "integrity": "sha512-kM3ee93xDLnhu7R1j2BpJ+0zenlOB5ZE6H+vt2iCNXdGgcxedzweZn6UeW5p2iJEdkNYaXDoJm8uoSLiXF4eBw==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "array-unique": "^0.3.2", + "component-emitter": "^1.2.1", + "export-files": "^2.1.1", + "extend-shallow": "^2.0.1", + "fs-exists-sync": "^0.1.0", + "get-value": "^2.0.6", + "kind-of": "^3.0.4", + "lazy-cache": "^2.0.1", + "map-schema": "^0.2.3", + "minimist": "^1.2.0", + "mixin-deep": "^1.1.3", + "omit-empty": "^0.4.1", + "parse-git-config": "^1.0.2", + "repo-utils": "^0.3.6", + "semver": "^5.3.0", + "stringify-author": "^0.1.3", + "write-json": "^0.2.2" + }, + "bin": { + "normalize-pkg": "cli.js" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/normalize-pkg/node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-pkg/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/now-and-later": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-0.0.6.tgz", + "integrity": "sha512-qNIeNeH6v6KbriliCoOEmKhelv+66P2yCKEQta3MYcwN98S3NrVMgYEh9hWxJRPqPna3d7r0KElZQKQkAm0/jA==", + "license": "MIT", + "dependencies": { + "once": "^1.3.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", + "license": "MIT", + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-visit/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries/node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.groupby/node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha512-UiAM5mhmIuKLsOvrL+B0U2d1hXHF3bFYWIuH1LMpuV2EJEHG1Ntz06PgLEHjm6VFd87NpH8rastvPoyv6UW2fA==", + "license": "MIT", + "dependencies": { + "for-own": "^0.1.4", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/omit-empty": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/omit-empty/-/omit-empty-0.4.1.tgz", + "integrity": "sha512-NwnVOAaLwUEYmvvwLKKqvG6BkSG0pu0yKhKc6uYbWerkIXe6Wi2HQ1qoL+Wksj3DCauRuNKIjZUsLyjLj1/lrw==", + "license": "MIT", + "dependencies": { + "has-values": "^0.1.4", + "kind-of": "^3.0.3", + "reduce-object": "^0.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha512-GZ+g4jayMqzCRMgB2sol7GiCLjKfS1PINkjmx8spcKce1LiVqcbQreXwqs2YAFXC6R03VIG28ZS31t8M866v6A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/option-cache": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/option-cache/-/option-cache-3.5.0.tgz", + "integrity": "sha512-Hr14410H8ajAHeUirXZtuE9drwy8e85l0CssHB/k7Y6nRkleKsGAzB/gwltUzsnIqr9Y+7ZQ+H16GYWAJH3PVg==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.3", + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^0.3.1", + "kind-of": "^3.2.2", + "lazy-cache": "^2.0.2", + "set-value": "^0.4.3", + "to-object-path": "^0.3.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/option-cache/node_modules/set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha512-2Z0LRUUvYeF7gIFFep48ksPq0NR09e5oKoFXznaMGNcu+EZAfGnyL0K6xno2gCqX6dZYEZRjrcn04/gvZzcKhQ==", + "deprecated": "Critical bug fixed in v3.0.1, please upgrade to the latest version.", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ordered-read-streams": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz", + "integrity": "sha512-xQvd8qvx9U1iYY9aVqPpoF5V9uaWJKV6ZGljkh/jkiNX0DiQsjbWvRumbh10QTMDE8DheaOEU8xi0szbrgjzcw==", + "license": "MIT", + "dependencies": { + "is-stream": "^1.0.1", + "readable-stream": "^2.0.1" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pad-right": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/pad-right/-/pad-right-0.2.2.tgz", + "integrity": "sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g==", + "license": "MIT", + "dependencies": { + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/paginationator": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/paginationator/-/paginationator-0.1.4.tgz", + "integrity": "sha512-o46P8Z9DK0blcmY7F95SnsBWZ6bow3HAcLKXlgIc/SZE8og21qrxL14nAi6Wy8E0Iw06wA0yS5icSayXw8BU8A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-author": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-author/-/parse-author-1.0.0.tgz", + "integrity": "sha512-OrNKo0jTFjJNCT0UKOPtnUctvGJvKdfB5ild+r3xwg/TgU5k2CCZW4fU9uJdKJ3njVFw5InP/2gd+n2vEXKgLQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-git-config": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-git-config/-/parse-git-config-1.1.1.tgz", + "integrity": "sha512-S3LGXJZVSy/hswvbSkfdbKBRVsnqKrVu6j8fcvdtJ4TxosSELyQDsJPuGPXuZ+EyuYuJd3O4uAF8gcISR0OFrQ==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "fs-exists-sync": "^0.1.0", + "git-config-path": "^1.0.1", + "ini": "^1.3.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-github-url": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-0.3.2.tgz", + "integrity": "sha512-vawkgsrRR8wm/nqFTVQIl9G/VkRJK2VVo0ECPni20WRV+NOmHXGilnWwC/EjVqRqQ4oSIKwRKP1jW8CjlxlJ2Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha512-FC5TeK0AwXzq3tUBFtH74naWkPQCEWs4K+xMxWZBlKDWu0bVHXGZa+KKqxKidd7xwhdZ19ZNuF2uO1M/r196HA==", + "license": "MIT", + "dependencies": { + "glob-base": "^0.3.0", + "is-dotfile": "^1.0.0", + "is-extglob": "^1.0.0", + "is-glob": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-glob/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-glob/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parser-front-matter": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/parser-front-matter/-/parser-front-matter-1.6.4.tgz", + "integrity": "sha512-eqtUnI5+COkf1CQOYo8FmykN5Zs+5Yr60f/7GcPgQDZEEjdE/VZ4WMaMo9g37foof8h64t/TH2Uvk2Sq0fDy/g==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "file-is-binary": "^1.0.0", + "gray-matter": "^3.0.2", + "isobject": "^3.0.1", + "lazy-cache": "^2.0.2", + "mixin-deep": "^1.2.0", + "trim-leading-lines": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parser-front-matter/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "license": "MIT", + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-to-regexp/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-store": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/pkg-store/-/pkg-store-0.2.2.tgz", + "integrity": "sha512-1JZVLbIRN6Dgsfk918EMZyL/T4NvJduSaT7n6ssHO3FV1FCrg6zjHJmuj3+Fb/Y5nBe3IBDoMYsY6Jf2IoRH0A==", + "license": "MIT", + "dependencies": { + "cache-base": "^0.8.2", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "union-value": "^0.2.3", + "write-json": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-store/node_modules/cache-base": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-0.8.5.tgz", + "integrity": "sha512-19t0n7xdoVr5Q08+6sF85YZ9VuvbpVFq5JLm0gcsRmCvTO1Y3duTJGMaOQYf14Ras4o6dEnvoqvjdrUK1tNtgg==", + "license": "MIT", + "dependencies": { + "collection-visit": "^0.2.1", + "component-emitter": "^1.2.1", + "get-value": "^2.0.5", + "has-value": "^0.3.1", + "isobject": "^3.0.0", + "lazy-cache": "^2.0.1", + "set-value": "^0.4.2", + "to-object-path": "^0.3.0", + "union-value": "^0.2.3", + "unset-value": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-store/node_modules/cache-base/node_modules/lazy-cache": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", + "integrity": "sha512-7vp2Acd2+Kz4XkzxGxaB1FWOi8KjWIWsgdfD5MCb86DWvlLqhRPM+d6Pro3iNEL5VT9mstz5hKAlcd+QR6H3aA==", + "license": "MIT", + "dependencies": { + "set-getter": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-store/node_modules/collection-visit": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-0.2.3.tgz", + "integrity": "sha512-V88PJOCqJfsZS45YBELDgmhQkECokQAAr9XR4hT6eFkFsAPsCsk3EoDHSuBPYzygjquGM/0KF4vdwTiQO6lbdw==", + "license": "MIT", + "dependencies": { + "lazy-cache": "^2.0.1", + "map-visit": "^0.1.5", + "object-visit": "^0.3.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-store/node_modules/collection-visit/node_modules/lazy-cache": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", + "integrity": "sha512-7vp2Acd2+Kz4XkzxGxaB1FWOi8KjWIWsgdfD5MCb86DWvlLqhRPM+d6Pro3iNEL5VT9mstz5hKAlcd+QR6H3aA==", + "license": "MIT", + "dependencies": { + "set-getter": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-store/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/pkg-store/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-store/node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-store/node_modules/map-visit": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-0.1.5.tgz", + "integrity": "sha512-zdmJBFvvVR/H5wCfsCP7XxSLp+346yAZ30Wy2OsQLcH19OVGMWa3Ms9quO00lj9ybsySu3gKOINNgICb4Zqauw==", + "license": "MIT", + "dependencies": { + "lazy-cache": "^2.0.1", + "object-visit": "^0.3.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-store/node_modules/map-visit/node_modules/lazy-cache": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", + "integrity": "sha512-7vp2Acd2+Kz4XkzxGxaB1FWOi8KjWIWsgdfD5MCb86DWvlLqhRPM+d6Pro3iNEL5VT9mstz5hKAlcd+QR6H3aA==", + "license": "MIT", + "dependencies": { + "set-getter": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-store/node_modules/object-visit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-0.3.4.tgz", + "integrity": "sha512-6QNyX7uTuwqxP7pmDBqgBDKdmZws1rXriUyXM5KG6+7J0aYRuuAGoc636IGdLzgOL77WUwL+EpoTJrEHwWsyOA==", + "license": "MIT", + "dependencies": { + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-store/node_modules/object-visit/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "license": "MIT", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-store/node_modules/set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha512-2Z0LRUUvYeF7gIFFep48ksPq0NR09e5oKoFXznaMGNcu+EZAfGnyL0K6xno2gCqX6dZYEZRjrcn04/gvZzcKhQ==", + "deprecated": "Critical bug fixed in v3.0.1, please upgrade to the latest version.", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-store/node_modules/union-value": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-0.2.4.tgz", + "integrity": "sha512-Tv3cqdyY8yjW9ZcJ9WP7JdHS34natzylD0oNRLlYbWOfUdC4EQ0sf3fubnqrK2IErtlmobFmuS1pWvv88VghpA==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pkg-store/node_modules/unset-value": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-0.1.2.tgz", + "integrity": "sha512-yhv5I4TsldLdE3UcVQn0hD2T5sNCPv4+qm/CTUpRKIpwthYRIipsAPdsrNpOI79hPQa0rTTeW22Fq6JWRcTgNg==", + "license": "MIT", + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "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" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha512-s/46sYeylUfHNjI+sA/78FAHlmIuKqI9wNnzEOGehAlUUYeObv5C2mOinXBjyUyWmJ2SfcS2/ydApH4hTF4WXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-time": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-0.2.0.tgz", + "integrity": "sha512-BwYVCPtnSq3nIGDK2rgwZTN2ClhBQmnG8pudrXIfGBwuMutIBj/W7wm/jz1WCHl/Kk2Q5i1Am1uD2Q74oPyBCw==", + "license": "MIT", + "dependencies": { + "is-number": "^2.0.2", + "nanoseconds": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-time/node_modules/is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/project-name": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/project-name/-/project-name-0.2.6.tgz", + "integrity": "sha512-ZOxqunIi7fnAX+E0tE+FLHv2pSEa7IgEbnVG2s4wPxWL+p2cUk9KRDZV4lNkpfyrVR6rfOUBxIbctbJDo/qOTA==", + "license": "MIT", + "dependencies": { + "find-pkg": "^0.1.2", + "git-repo-name": "^0.6.0", + "minimist": "^1.2.0" + }, + "bin": { + "project-name": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/question-cache": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/question-cache/-/question-cache-0.4.0.tgz", + "integrity": "sha512-QgX1mI/ZNBbG8M5gYfZQG/qxZRggP2Fk+WOqE/FKylmNwi5aWy6o1JSaojYrHT5JUtRdyG+wwVJSlTfW7UBmog==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "arr-union": "^3.1.0", + "async": "1.5.2", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "get-value": "^2.0.5", + "has-value": "^0.3.1", + "inquirer2": "^0.1.1", + "is-answer": "^0.1.0", + "isobject": "^2.0.0", + "lazy-cache": "^1.0.3", + "mixin-deep": "^1.1.3", + "omit-empty": "^0.3.6", + "option-cache": "^3.3.5", + "os-homedir": "^1.0.1", + "project-name": "^0.2.4", + "set-value": "^0.3.3", + "to-choices": "^0.2.0", + "use": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/question-cache/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/question-cache/node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/question-cache/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/question-cache/node_modules/omit-empty": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/omit-empty/-/omit-empty-0.3.6.tgz", + "integrity": "sha512-P5zl3TYREgcRAjjyj9kYHNhVtOOXMlCyYh/KNm53oUZNKpGOBbS0WLdRcThDPWbuFleXlbCd1KTBRZD86nj3RA==", + "license": "MIT", + "dependencies": { + "has-values": "^0.1.4", + "is-date-object": "^1.0.1", + "isobject": "^2.0.0", + "reduce-object": "^0.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/question-cache/node_modules/set-value": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.3.3.tgz", + "integrity": "sha512-aJPTd11HzK47w8xJMpyY4tBmFC6EidC8EG2fENxCJvPwLYzXLnNaesgo796y1fhSISSYAuah4Het+wDoPXK2tg==", + "deprecated": "Critical bug fixed in v3.0.1, please upgrade to the latest version.", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "isobject": "^2.0.0", + "to-object-path": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/question-cache/node_modules/to-object-path": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.2.0.tgz", + "integrity": "sha512-6oMu4CTicplxUMOXBoS1W9YNjIclUzmWpWf02v+JnYMEGVX24rTCsYMHay85WA7Wq+9wZa2iJ+HAAX0yGOcxCQ==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "is-arguments": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/question-store": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/question-store/-/question-store-0.11.1.tgz", + "integrity": "sha512-rvyFpqLYQCO7FOnX+3qZ7b8K7omWkn9MWyj/7dknf7BaGZHo//fzBS2/0atmcvZfjT2mu1q64oiZIrsB7OqqGg==", + "license": "MIT", + "dependencies": { + "common-config": "^0.1.0", + "data-store": "^0.16.1", + "debug": "^2.2.0", + "is-answer": "^0.1.0", + "lazy-cache": "^2.0.1", + "project-name": "^0.2.6", + "question-cache": "^0.5.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/question-store/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/question-store/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/question-store/node_modules/question-cache": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/question-cache/-/question-cache-0.5.1.tgz", + "integrity": "sha512-v9F1LnlSQIUEAGFtrfVX/76lH4u4zyV34t94o6EkguPTKKfbvV6SLH8h3pn7LXGZLmAgD1PbmVOuKMY8ZWnuPg==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "arr-union": "^3.1.0", + "async-each-series": "^1.1.0", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "get-value": "^2.0.6", + "has-value": "^0.3.1", + "inquirer2": "^0.1.1", + "is-answer": "^0.1.0", + "isobject": "^2.1.0", + "lazy-cache": "^2.0.1", + "mixin-deep": "^1.1.3", + "omit-empty": "^0.4.1", + "option-cache": "^3.4.0", + "os-homedir": "^1.0.1", + "project-name": "^0.2.5", + "set-value": "^0.3.3", + "to-choices": "^0.2.0", + "use": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/question-store/node_modules/set-value": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.3.3.tgz", + "integrity": "sha512-aJPTd11HzK47w8xJMpyY4tBmFC6EidC8EG2fENxCJvPwLYzXLnNaesgo796y1fhSISSYAuah4Het+wDoPXK2tg==", + "deprecated": "Critical bug fixed in v3.0.1, please upgrade to the latest version.", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "isobject": "^2.0.0", + "to-object-path": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/question-store/node_modules/to-object-path": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.2.0.tgz", + "integrity": "sha512-6oMu4CTicplxUMOXBoS1W9YNjIclUzmWpWf02v+JnYMEGVX24rTCsYMHay85WA7Wq+9wZa2iJ+HAAX0yGOcxCQ==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "is-arguments": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/question-store/node_modules/use": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/use/-/use-2.0.2.tgz", + "integrity": "sha512-RrhWfFWkNCz3djfSFZh7uSwu491QRhwNaHyAgB2sGl4kmmznb5ZUuuHpiWLVEsXOdpDakYK/x5+9o4lgg41UMw==", + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "lazy-cache": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/question-store/node_modules/use/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randomatic": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", + "license": "MIT", + "dependencies": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/randomatic/node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/randomatic/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "dev": true, + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-file": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/read-file/-/read-file-0.2.0.tgz", + "integrity": "sha512-na/zgd5KplGlR+io+ygXQMIoDfX/Y0bNS5+P2TOXOTk5plquOVd0snudCd30hZJAsnVK2rxuxUP2z0CN+Aw1lQ==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readline2": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", + "integrity": "sha512-8/td4MmwUB6PkZUbV25uKz7dfrmjYWxsW8DVfibWdlHRk/l/DfHKn4pU+dfcoGLFgWOdyGCzINRQD7jn+Bv+/g==", + "license": "MIT", + "dependencies": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "mute-stream": "0.0.5" + } + }, + "node_modules/readline2/node_modules/is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "license": "MIT", + "dependencies": { + "number-is-nan": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reduce-object": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/reduce-object/-/reduce-object-0.1.3.tgz", + "integrity": "sha512-7js/WmWoI5NRe/mfxUimt0rmj04lfhJIa8SDyt+OKasagu+KjffnVxElTKuZs1fRjytlN46BrDoVK+IsBVovtw==", + "dependencies": { + "for-own": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reflect.getprototypeof/node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "license": "MIT", + "dependencies": { + "is-equal-shallow": "^0.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/relative": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/relative/-/relative-3.0.2.tgz", + "integrity": "sha512-Q5W2qeYtY9GbiR8z1yHNZ1DGhyjb4AnLEjt8iE6XfcC1QIu+FAtj3HQaO0wH28H1mX6cqNLvAqWhP402dxJGyA==", + "license": "MIT", + "dependencies": { + "isobject": "^2.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/remote-origin-url": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/remote-origin-url/-/remote-origin-url-0.5.3.tgz", + "integrity": "sha512-crQ7Xk1m/F2IiwBx5oTqk/c0hjoumrEz+a36+ZoVupskQRE/q7pAwHKsTNeiZ31sbSTELvVlVv4h1W0Xo5szKg==", + "license": "MIT", + "dependencies": { + "parse-git-config": "^1.1.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "license": "ISC" + }, + "node_modules/repeat-element": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", + "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/replace-ext": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha512-AFBWBy9EVRTa/LhEcG8QDP3FvpwZqmvN2QFDuJswFeaVhWnZMp8q3E6Zd90SR04PlIwfGdyVjNyLPyen/ek5CQ==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/repo-utils": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/repo-utils/-/repo-utils-0.3.7.tgz", + "integrity": "sha512-NQmnug1GX04LoNb2bXGsCV3FzLDqmwf3qMmjToibrxI1CFV2uyE2XDdo9SYW8epfBK7wmw0ANhkmDtbGlrkyWQ==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "get-value": "^2.0.6", + "git-config-path": "^1.0.1", + "is-absolute": "^0.2.6", + "kind-of": "^3.0.4", + "lazy-cache": "^2.0.1", + "mixin-deep": "^1.1.3", + "omit-empty": "^0.4.1", + "parse-author": "^1.0.0", + "parse-git-config": "^1.0.2", + "parse-github-url": "^0.3.2", + "project-name": "^0.2.6" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", + "integrity": "sha512-QxMPqI6le2u0dCLyiGzgy92kjkkL6zO0XyvHzjdTNH3zM6e5Hz3BwG6+aEyNgiQ5Xz6PwTwgQEj3U50dByPKIA==", + "license": "MIT", + "dependencies": { + "expand-tilde": "^1.2.2", + "global-modules": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-file": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/resolve-file/-/resolve-file-0.2.2.tgz", + "integrity": "sha512-3t2k4iUeMlX3PbjgZPcKzILg8HEtl0VW/lS8G+k4FCgj3kNn1uTOv6YJtm192rYMFpq9abzfJ2xd5W6ibOwVag==", + "license": "MIT", + "dependencies": { + "cwd": "^0.10.0", + "expand-tilde": "^2.0.1", + "extend-shallow": "^2.0.1", + "fs-exists-sync": "^0.1.0", + "global-modules": "^0.2.3", + "homedir-polyfill": "^1.0.0", + "lazy-cache": "^2.0.1", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-file/node_modules/cwd": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", + "integrity": "sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA==", + "license": "MIT", + "dependencies": { + "find-pkg": "^0.1.2", + "fs-exists-sync": "^0.1.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/resolve-file/node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-glob/-/resolve-glob-1.0.0.tgz", + "integrity": "sha512-wSW9pVGJRs89k0wEXhM7C6+va9998NsDhgc0Y+6Nv8hrHsu0hUS7Ug10J1EiVtU6N2tKlSNvx9wLihL8Ao22Lg==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-valid-glob": "^1.0.0", + "matched": "^1.0.2", + "relative": "^3.0.2", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-glob/node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-glob/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/resolve-glob/node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "license": "MIT", + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-glob/node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-glob/node_modules/has-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-glob/-/has-glob-1.0.0.tgz", + "integrity": "sha512-D+8A457fBShSEI3tFCj65PAbT++5sKiFtdCdOam0gnfBgw9D277OERk+HM9qYJXmdVLZ/znez10SqHN0BBQ50g==", + "license": "MIT", + "dependencies": { + "is-glob": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-glob/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-glob/node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-glob/node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-glob/node_modules/matched": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/matched/-/matched-1.0.2.tgz", + "integrity": "sha512-7ivM1jFZVTOOS77QsR+TtYHH0ecdLclMkqbf5qiJdX2RorqfhsL65QHySPZgDE0ZjHoh+mQUNHTanNXIlzXd0Q==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "async-array-reduce": "^0.2.1", + "glob": "^7.1.2", + "has-glob": "^1.0.0", + "is-valid-glob": "^1.0.0", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/resolve-glob/node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-glob/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha512-reSjH4HuiFlxlaBaFCiS6O76ZGG2ygKoSlCsipKdaZuKSPx/+bt9mULkn4l0asVzbEfQQmXRg6Wp6gv6m0wElw==", + "license": "MIT", + "dependencies": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rethrow": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/rethrow/-/rethrow-0.2.3.tgz", + "integrity": "sha512-vtB0AIP/FlRbR4stc8szvHXe+N4158/K1hRMZbFHljIiQAHru54M9LylbxNjBGHl9biuwQNVUdvRzVxv1QWAiA==", + "license": "MIT", + "dependencies": { + "ansi-bgred": "^0.1.1", + "ansi-red": "^0.1.1", + "ansi-yellow": "^0.1.1", + "extend-shallow": "^1.1.4", + "lazy-cache": "^0.2.3", + "right-align": "^0.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rethrow/node_modules/extend-shallow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-1.1.4.tgz", + "integrity": "sha512-L7AGmkO6jhDkEBBGWlLtftA80Xq8DipnrRPr0pyi7GQLXkaq9JYA4xF4z6qnadIC6euiTDKco0cGSU9muw+WTw==", + "license": "MIT", + "dependencies": { + "kind-of": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rethrow/node_modules/kind-of": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "integrity": "sha512-aUH6ElPnMGon2/YkxRIigV32MOpTVcoXQ1Oo8aYn40s+sJ3j+0gFZsT8HKDcxNy7Fi9zuquWtGaGAahOdv5p/g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rethrow/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==", + "license": "MIT", + "dependencies": { + "align-text": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-async": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", + "integrity": "sha512-qOX+w+IxFgpUpJfkv2oGN0+ExPs68F4sZHfaRRx4dDexAQkG83atugKVEylyT5ARees3HBbfmuvnjbrd8j9Wjw==", + "license": "MIT", + "dependencies": { + "once": "^1.3.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rx-lite": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha512-Cun9QucwK6MIrp3mry/Y7hqD1oFqTYLQ4pGxaHTjIdaFDWRGGLikqp6u8LcWJnzpoALg9hap+JGk8sFIUuEGNA==" + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-getter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.1.tgz", + "integrity": "sha512-9sVWOy+gthr+0G9DzqqLaYNA7+5OKkSmcqjL9cBpDEaZrr3ShQlyX2cZ/O/ozE41oxn/Tt0LGEM/w4Rub3A3gw==", + "license": "MIT", + "dependencies": { + "to-object-path": "^0.3.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sort-object-arrays": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sort-object-arrays/-/sort-object-arrays-0.1.1.tgz", + "integrity": "sha512-yqoVMBF2wzCdE4f2zeYKq2dQHe1WjGIdAV1dYSkXOFB+M3Bo+Bp0u+NdZCOETM3OC1VXerlruTD6Ckgus1NsnA==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-string/node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-string/node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-string/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/src-stream": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/src-stream/-/src-stream-0.1.1.tgz", + "integrity": "sha512-fczCn/BzNcH27V7unPzgCl+owTuC/Uv3UG9BQxGemRs6Fy1M2GFmYu1ZHQ2UjeYlGQqAmkModp949g235kYzcw==", + "license": "MIT", + "dependencies": { + "duplexify": "^3.4.2", + "merge-stream": "^0.1.8", + "through2": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/stream-combiner": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==", + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + }, + "node_modules/stream-exhaust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", + "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", + "license": "MIT" + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes/node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.matchall/node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.repeat/node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim/node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-author": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/stringify-author/-/stringify-author-0.1.3.tgz", + "integrity": "sha512-OxmcAnr4DESGl/ics9lAv30DdOBC2bdqswEAzTiOZSQRqVpWfnmlr3cpfxTmExf7phS5WxBJ1flD1e3ResNTBA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-bom-buffer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-bom-buffer/-/strip-bom-buffer-0.1.1.tgz", + "integrity": "sha512-dbIOX/cOLFgLH/2ofd7n78uPD3uPkXyt3P1IgaVoGiPYEdOnb7D1mawyhOTXyYWva1kCuRxJY5FkMsVKYlZRRg==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.0", + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-bom-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", + "integrity": "sha512-7jfJB9YpI2Z0aH3wu10ZqitvYJaE0s5IzFuWE+0pbb4Q/armTloEUShymkDO47YSLnjAW52mlXT//hs9wXNNJQ==", + "license": "MIT", + "dependencies": { + "first-chunk-stream": "^1.0.0", + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-bom-stream/node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "license": "MIT", + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-color": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/strip-color/-/strip-color-0.1.0.tgz", + "integrity": "sha512-p9LsUieSjWNNAxVCXLeilaDlmuUOrDS5/dF9znM1nZc7EGX5+zEFC0bEevsNIaldjlks+2jns5Siz6F9iK6jwA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/success-symbol": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/success-symbol/-/success-symbol-0.1.0.tgz", + "integrity": "sha512-7S6uOTxPklNGxOSbDIg4KlVLBQw1UiGVyfCUYgYxrZUKRblUkmGj7r8xlfQoFudvqLv6Ap5gd76/IIFfI9JG2A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tableize-object": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tableize-object/-/tableize-object-0.1.0.tgz", + "integrity": "sha512-seDB76zNqvGXG0W8gxUteRuq1fk1dvSxcRVbeYQ1a1QqMkbtqrGwvqTubfN6VCizzlb7NxOPM/j3z9JeBrbxYg==", + "license": "MIT", + "dependencies": { + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/template-error": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/template-error/-/template-error-0.1.2.tgz", + "integrity": "sha512-soS5m+iT4k/okmMyydvMjPlmyz3CowvMcOxfgoAqccmkyF81W3D+zMi4lhqbSIhTgLhKE/Bh8wUlXzr6F+ERCw==", + "license": "MIT", + "dependencies": { + "engine": "^0.1.5", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "rethrow": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/template-error/node_modules/kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==", + "license": "MIT", + "dependencies": { + "is-buffer": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/template-error/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/templates": { + "version": "0.24.3", + "resolved": "https://registry.npmjs.org/templates/-/templates-0.24.3.tgz", + "integrity": "sha512-R5CUlz3atppbifPePB5Z2KGXCsB0Y87lQ/+ziizq/d3kyydDlNk40yX98RWLprNnKjTiwqeiuGjLJlPPJPYshg==", + "license": "MIT", + "dependencies": { + "array-sort": "^0.1.2", + "async-each": "^1.0.0", + "base": "^0.11.1", + "base-data": "^0.6.0", + "base-engines": "^0.2.0", + "base-helpers": "^0.1.1", + "base-option": "^0.8.3", + "base-plugins": "^0.4.13", + "base-routes": "^0.2.1", + "debug": "^2.2.0", + "deep-bind": "^0.3.0", + "define-property": "^0.2.5", + "engine-base": "^0.1.2", + "export-files": "^2.1.1", + "extend-shallow": "^2.0.1", + "falsey": "^0.3.0", + "get-value": "^2.0.6", + "get-view": "^0.1.1", + "group-array": "^0.3.0", + "has-glob": "^0.1.1", + "has-value": "^0.3.1", + "inflection": "^1.10.0", + "is-valid-app": "^0.2.0", + "layouts": "^0.11.0", + "lazy-cache": "^2.0.1", + "match-file": "^0.2.0", + "mixin-deep": "^1.1.3", + "paginationator": "^0.1.3", + "pascalcase": "^0.1.1", + "set-value": "^0.3.3", + "template-error": "^0.1.2", + "vinyl-item": "^0.1.0", + "vinyl-view": "^0.1.2" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/templates/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/templates/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/templates/node_modules/set-value": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.3.3.tgz", + "integrity": "sha512-aJPTd11HzK47w8xJMpyY4tBmFC6EidC8EG2fENxCJvPwLYzXLnNaesgo796y1fhSISSYAuah4Het+wDoPXK2tg==", + "deprecated": "Critical bug fixed in v3.0.1, please upgrade to the latest version.", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "isobject": "^2.0.0", + "to-object-path": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/templates/node_modules/to-object-path": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.2.0.tgz", + "integrity": "sha512-6oMu4CTicplxUMOXBoS1W9YNjIclUzmWpWf02v+JnYMEGVX24rTCsYMHay85WA7Wq+9wZa2iJ+HAAX0yGOcxCQ==", + "license": "MIT", + "dependencies": { + "arr-flatten": "^1.0.1", + "is-arguments": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2-filter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", + "integrity": "sha512-miwWajb1B80NvIVKXFPN/o7+vJc4jYUvnZCwvhicRAoTxdD9wbcjri70j+BenCrN/JXEPKDjhpw4iY7yiNsCGg==", + "license": "MIT", + "dependencies": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "node_modules/time-diff": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/time-diff/-/time-diff-0.3.1.tgz", + "integrity": "sha512-8/LJTO3zKbhj6sQFeN3aoAA04GGjUgwKEquQVnKXkziHjEHadpIVIQ1rAjQgSVMnBRubJ/q5gMjK9WqXTzSykA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^2.1.0", + "log-utils": "^0.1.0", + "pretty-time": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/time-diff/node_modules/ansi-colors": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-0.1.0.tgz", + "integrity": "sha512-nUNbMZLDr1YQaPdMC2lREJXKttoaHwICajt9x40Js/POX7gNv7OK/VbC9ciJaIFshg9Xol+1GclqfY14UW+0ZA==", + "license": "MIT", + "dependencies": { + "ansi-bgblack": "^0.1.1", + "ansi-bgblue": "^0.1.1", + "ansi-bgcyan": "^0.1.1", + "ansi-bggreen": "^0.1.1", + "ansi-bgmagenta": "^0.1.1", + "ansi-bgred": "^0.1.1", + "ansi-bgwhite": "^0.1.1", + "ansi-bgyellow": "^0.1.1", + "ansi-black": "^0.1.1", + "ansi-blue": "^0.1.1", + "ansi-bold": "^0.1.1", + "ansi-cyan": "^0.1.1", + "ansi-dim": "^0.1.1", + "ansi-gray": "^0.1.1", + "ansi-green": "^0.1.1", + "ansi-grey": "^0.1.1", + "ansi-hidden": "^0.1.1", + "ansi-inverse": "^0.1.1", + "ansi-italic": "^0.1.1", + "ansi-magenta": "^0.1.1", + "ansi-red": "^0.1.1", + "ansi-reset": "^0.1.1", + "ansi-strikethrough": "^0.1.1", + "ansi-underline": "^0.1.1", + "ansi-white": "^0.1.1", + "ansi-yellow": "^0.1.1", + "lazy-cache": "^0.2.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/time-diff/node_modules/is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/time-diff/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/time-diff/node_modules/log-utils": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/log-utils/-/log-utils-0.1.5.tgz", + "integrity": "sha512-5jLIj9RWWYxQbBhHDvNZTZE3J/oSTbw/fuPmsXJg8/vbY/4XiJ4YAiEPrwo3dLbcB/n9k1qTznOVr6IigiaF7A==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^0.1.0", + "error-symbol": "^0.1.0", + "info-symbol": "^0.1.0", + "log-ok": "^0.1.1", + "success-symbol": "^0.1.0", + "time-stamp": "^1.0.1", + "warning-symbol": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha512-gLCeArryy2yNTRzTGKbZbloctj64jkZ57hj5zdraXue6aFgd6PmvVtEyiUU+hvU0v7q08oVv8r8ev0tRo6bvgw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-absolute-glob": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz", + "integrity": "sha512-Vvl5x6zNf9iVG1QTWeknmWrKzZxaeKfIDRibrZCR3b2V/2NlFJuD2HV7P7AVjaKLZNqLPHqyr0jGrW0fTcxCPQ==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-choices": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/to-choices/-/to-choices-0.2.0.tgz", + "integrity": "sha512-oPVwP4jpJZM4R3Yvfcod8/OjddMoi33amdFzwZktcHAjddmIEAzQ9DQsdPKUr/Q4hLxNMWPys4Pn1qJdLiR4Kg==", + "license": "MIT", + "dependencies": { + "ansi-gray": "^0.1.1", + "mixin-deep": "^1.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-file": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/to-file/-/to-file-0.2.0.tgz", + "integrity": "sha512-xLyYVRKJQTwy2tKMOLD0M0yL+YSZVgMAzkaY9hh7GhzgBBHSIWARDkgPx8krPPm0mW5CgoIFsQEdKRFOyIRdqg==", + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "file-contents": "^0.2.4", + "glob-parent": "^2.0.0", + "is-valid-glob": "^0.3.0", + "isobject": "^2.1.0", + "lazy-cache": "^2.0.1", + "vinyl": "^1.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-file/node_modules/glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha512-JDYOvfxio/t42HKdxkAYaCiBN7oYiuxykOxKxdaUW5Qn0zaYN3gRQWolrwdnf0shM9/EP0ebuuTmyoXNr1cC5w==", + "license": "ISC", + "dependencies": { + "is-glob": "^2.0.0" + } + }, + "node_modules/to-file/node_modules/is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-file/node_modules/is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-leading-lines": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/trim-leading-lines/-/trim-leading-lines-0.1.1.tgz", + "integrity": "sha512-ViFS8blDWJN4Jg10fyZ+sIAfkSSAn5NiTVywc3kKtMWK3DZjaV7FV86oX3i9KY6/gqYkdka/UNeM2/NMGttiyA==", + "license": "MIT", + "dependencies": { + "is-whitespace": "^0.3.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "dev": true, + "license": "MIT" + }, + "node_modules/union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unique-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.4.0.tgz", + "integrity": "sha512-V6QarSfeSgDipGA9EZdoIzu03ZDlOFkk+FbEP5cwgrZXN3iIkYR91IjU2EnM6rB835kGQsqHX8qncObTXV+6KA==", + "license": "MIT", + "dependencies": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "3.0.0" + } + }, + "node_modules/unique-stream/node_modules/through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "license": "MIT", + "dependencies": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", + "license": "MIT", + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/update/-/update-0.7.4.tgz", + "integrity": "sha512-B7HArWh4T6TSmMffmxlbD9gZM0QdboQ8N/p5aHcyhGCuuVRHSk37pvuQlAvi1XBrQMrEX5WJUQyQR8+jy/x4iQ==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "assemble-core": "^0.25.0", + "assemble-loader": "^0.6.1", + "base-cli-process": "^0.1.18", + "base-config-process": "^0.1.9", + "base-generators": "^0.4.5", + "base-questions": "^0.7.3", + "base-runtimes": "^0.2.0", + "base-store": "^0.4.4", + "common-config": "^0.1.0", + "data-store": "^0.16.1", + "export-files": "^2.1.1", + "extend-shallow": "^2.0.1", + "find-pkg": "^0.1.2", + "fs-exists-sync": "^0.1.0", + "global-modules": "^0.2.2", + "gulp-choose-files": "^0.1.3", + "is-valid-app": "^0.2.0", + "isobject": "^2.1.0", + "lazy-cache": "^2.0.1", + "log-utils": "^0.2.1", + "parser-front-matter": "^1.4.1", + "resolve-dir": "^0.1.0", + "resolve-file": "^0.2.0", + "set-blocking": "^2.0.0", + "strip-color": "^0.1.0", + "text-table": "^0.2.0", + "through2": "^2.0.1", + "yargs-parser": "^2.4.1" + }, + "bin": { + "update": "bin/update.js" + }, + "engines": { + "node": ">=5.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "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" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==", + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use/-/use-1.1.2.tgz", + "integrity": "sha512-25Uw2xiVk0m2ySqmnu2GjOIROlImdXMRcpI6Cq7sZeG/zFZgFkSeo2+QwKNWJncfZOVS55eACoinvJ3EtprOBw==", + "license": "MIT", + "dependencies": { + "define-property": "^0.2.5", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/vali-date": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", + "integrity": "sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha512-Ci3wnR2uuSAWFMSglZuB8Z2apBdtOyz8CV7dC6/U1XbltXBC+IuutUkXQISz01P+US2ouBuesSbV6zILZ6BuzQ==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.0", + "clone-stats": "^0.0.1", + "replace-ext": "0.0.1" + }, + "engines": { + "node": ">= 0.9" + } + }, + "node_modules/vinyl-fs": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz", + "integrity": "sha512-lxMlQW/Wxk/pwhooY3Ut0Q11OH5ZvZfV0Gg1c306fBNWznQ6ZeQaCdE7XX0O/PpGSqgAsHMBxwFgcGxiYW3hZg==", + "license": "MIT", + "dependencies": { + "duplexify": "^3.2.0", + "glob-stream": "^5.3.2", + "graceful-fs": "^4.0.0", + "gulp-sourcemaps": "1.6.0", + "is-valid-glob": "^0.3.0", + "lazystream": "^1.0.0", + "lodash.isequal": "^4.0.0", + "merge-stream": "^1.0.0", + "mkdirp": "^0.5.0", + "object-assign": "^4.0.0", + "readable-stream": "^2.0.4", + "strip-bom": "^2.0.0", + "strip-bom-stream": "^1.0.0", + "through2": "^2.0.0", + "through2-filter": "^2.0.0", + "vali-date": "^1.0.0", + "vinyl": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/vinyl-fs/node_modules/merge-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", + "integrity": "sha512-e6RM36aegd4f+r8BZCcYXlO2P3H6xbUM6ktL2Xmf45GAOit9bI4z6/3VU7JwllVO1L7u0UDSg/EhzQ5lmMLolA==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.1" + } + }, + "node_modules/vinyl-fs/node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "license": "MIT", + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-item": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/vinyl-item/-/vinyl-item-0.1.0.tgz", + "integrity": "sha512-9L2HEcbtuTdKCLWDucRPObPoAxnUUCdAXg0QDf3aDPM3oFpb6C+yct/R31PA9EhLGeilNl8TF/inc3OwFSSEMg==", + "license": "MIT", + "dependencies": { + "base": "^0.8.1", + "base-option": "^0.8.2", + "base-plugins": "^0.4.12", + "clone": "^1.0.2", + "clone-stats": "^1.0.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "isobject": "^2.1.0", + "lazy-cache": "^2.0.1", + "vinyl": "^1.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-item/node_modules/base": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/base/-/base-0.8.1.tgz", + "integrity": "sha512-hCEtSWF9Xin1mVIrgCAwJhIJxURWOu3odjKsv+9TXofdJly0vO9Di87hnkChwi44v0+LPzHtNOjoCUYb36fBhg==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "cache-base": "^0.8.2", + "class-utils": "^0.3.2", + "component-emitter": "^1.2.0", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "lazy-cache": "^1.0.3", + "mixin-deep": "^1.1.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-item/node_modules/base/node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-item/node_modules/cache-base": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-0.8.5.tgz", + "integrity": "sha512-19t0n7xdoVr5Q08+6sF85YZ9VuvbpVFq5JLm0gcsRmCvTO1Y3duTJGMaOQYf14Ras4o6dEnvoqvjdrUK1tNtgg==", + "license": "MIT", + "dependencies": { + "collection-visit": "^0.2.1", + "component-emitter": "^1.2.1", + "get-value": "^2.0.5", + "has-value": "^0.3.1", + "isobject": "^3.0.0", + "lazy-cache": "^2.0.1", + "set-value": "^0.4.2", + "to-object-path": "^0.3.0", + "union-value": "^0.2.3", + "unset-value": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-item/node_modules/cache-base/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-item/node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", + "license": "MIT" + }, + "node_modules/vinyl-item/node_modules/collection-visit": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-0.2.3.tgz", + "integrity": "sha512-V88PJOCqJfsZS45YBELDgmhQkECokQAAr9XR4hT6eFkFsAPsCsk3EoDHSuBPYzygjquGM/0KF4vdwTiQO6lbdw==", + "license": "MIT", + "dependencies": { + "lazy-cache": "^2.0.1", + "map-visit": "^0.1.5", + "object-visit": "^0.3.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-item/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/vinyl-item/node_modules/map-visit": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-0.1.5.tgz", + "integrity": "sha512-zdmJBFvvVR/H5wCfsCP7XxSLp+346yAZ30Wy2OsQLcH19OVGMWa3Ms9quO00lj9ybsySu3gKOINNgICb4Zqauw==", + "license": "MIT", + "dependencies": { + "lazy-cache": "^2.0.1", + "object-visit": "^0.3.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-item/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/vinyl-item/node_modules/object-visit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-0.3.4.tgz", + "integrity": "sha512-6QNyX7uTuwqxP7pmDBqgBDKdmZws1rXriUyXM5KG6+7J0aYRuuAGoc636IGdLzgOL77WUwL+EpoTJrEHwWsyOA==", + "license": "MIT", + "dependencies": { + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-item/node_modules/set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha512-2Z0LRUUvYeF7gIFFep48ksPq0NR09e5oKoFXznaMGNcu+EZAfGnyL0K6xno2gCqX6dZYEZRjrcn04/gvZzcKhQ==", + "deprecated": "Critical bug fixed in v3.0.1, please upgrade to the latest version.", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-item/node_modules/union-value": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-0.2.4.tgz", + "integrity": "sha512-Tv3cqdyY8yjW9ZcJ9WP7JdHS34natzylD0oNRLlYbWOfUdC4EQ0sf3fubnqrK2IErtlmobFmuS1pWvv88VghpA==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-item/node_modules/unset-value": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-0.1.2.tgz", + "integrity": "sha512-yhv5I4TsldLdE3UcVQn0hD2T5sNCPv4+qm/CTUpRKIpwthYRIipsAPdsrNpOI79hPQa0rTTeW22Fq6JWRcTgNg==", + "license": "MIT", + "dependencies": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-item/node_modules/unset-value/node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vinyl-view": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/vinyl-view/-/vinyl-view-0.1.2.tgz", + "integrity": "sha512-qIc2qnXgOXZrT1Q1ViR1VMTjuylAi3Y/LSYSYfwJ6ZG7Ar5miUfioSIBu30bsHTo5dSz4ReDNSUw3lelCtc5Jw==", + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "engine-base": "^0.1.2", + "isobject": "^2.1.0", + "lazy-cache": "^2.0.1", + "mixin-deep": "^1.1.3", + "vinyl-item": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/warning-symbol": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/warning-symbol/-/warning-symbol-0.1.0.tgz", + "integrity": "sha512-1S0lwbHo3kNUKA4VomBAhqn4DPjQkIKSdbOin5K7EFUQNwyIKx+wZMGXKI53RUjla8V2B8ouQduUlgtx8LoSMw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + }, + "node_modules/write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha512-CJ17OoULEKXpA5pef3qLj5AxTJ6mSt7g84he2WIskKwqFO4T97d5V7Tadl0DYDk7qyUOQD5WlUlOMChaYrhxeA==", + "license": "MIT", + "dependencies": { + "mkdirp": "^0.5.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/write-json": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/write-json/-/write-json-0.2.2.tgz", + "integrity": "sha512-3HOXDnA8CgyaObzkxKPTHBw0feFlYMn9Mi8ZIrnoNJTTMABn+XOhmTsVlX/P/WeZuXEV9ApvQvR1fpZOOQ5FOg==", + "license": "MIT", + "dependencies": { + "write": "^0.2.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yargs-parser": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-2.4.1.tgz", + "integrity": "sha512-9pIKIJhnI5tonzG6OnCFlz/yln8xHYcGl+pn3xR0Vzff0vzN1PbNRaelgfgRUwZ3s4i3jvxT9WhmUGL4whnasA==", + "license": "ISC", + "dependencies": { + "camelcase": "^3.0.0", + "lodash.assign": "^4.0.6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000000000000000000000000000000000000..b1390c05ce8fbd4938a9f87d9de4e132e663d4a6 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,34 @@ +{ + "name": "genai-deepdetect-ui", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "framer-motion": "^12.38.0", + "lucide-react": "^0.323.0", + "next": "^15.5.14", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwind-merge": "^2.2.1", + "update": "^0.7.4" + }, + "devDependencies": { + "@types/node": "^20.11.5", + "@types/react": "^18.2.48", + "@types/react-dom": "^18.2.18", + "autoprefixer": "^10.4.17", + "eslint": "^8.56.0", + "eslint-config-next": "^15.5.14", + "postcss": "^8.4.33", + "tailwindcss": "^3.4.1", + "typescript": "^5.3.3" + } +} diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c2a0424e615c2ee8aef1d6080de49412637a81e5 --- /dev/null +++ b/frontend/pages/_app.tsx @@ -0,0 +1,18 @@ +import type { AppProps } from 'next/app' +import Head from 'next/head' +import '@/styles/globals.css' + +export default function App({ Component, pageProps }: AppProps) { + return ( + <> + + + + + + + + + + ) +} diff --git a/frontend/pages/_document.tsx b/frontend/pages/_document.tsx new file mode 100644 index 0000000000000000000000000000000000000000..10a60569d2fb89a876d86b1cbaa088d0a6a7ce05 --- /dev/null +++ b/frontend/pages/_document.tsx @@ -0,0 +1,20 @@ +import { Html, Head, Main, NextScript } from 'next/document' + +export default function Document() { + return ( + + + + + + + +
+ + + + ) +} diff --git a/frontend/pages/analyze.tsx b/frontend/pages/analyze.tsx new file mode 100644 index 0000000000000000000000000000000000000000..f8250df3c833692c974681a59ff522e86ba8852e --- /dev/null +++ b/frontend/pages/analyze.tsx @@ -0,0 +1,249 @@ +import type { NextPage } from 'next' +import Head from 'next/head' +import { useRouter } from 'next/router' +import { useState, useCallback, useRef } from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { ArrowLeft, Image as ImageIcon, Film } from 'lucide-react' +import type { AnalysisState, DetectionResponse } from '@/types/detection' +import { detectImage, detectVideo } from '@/lib/api' +import { formatBytes } from '@/lib/utils' +import UploadZone from '@/components/ui/upload-zone' +import LoadingOrbit from '@/components/ui/loading-orbit' +import VerdictBadge from '@/components/ui/verdict-badge' +import EngineBreakdown from '@/components/ui/engine-breakdown' +import MediaMetaStrip from '@/components/ui/media-meta-strip' +import ConfidenceRing from '@/components/ui/confidence-ring' + +const FADE_UP = { + initial: { opacity: 0, y: 14 }, + animate: { opacity: 1, y: 0 }, + transition: { duration: 0.4, ease: [0.25, 0.46, 0.45, 0.94] }, +} + +export default function Analyze(): JSX.Element { + const router = useRouter() + const defaultType = router.query.type === 'video' ? 'video' : 'image' + const [mediaType, setMediaType] = useState<'image' | 'video'>(defaultType) + const [state, setState] = useState({ status: 'idle' }) + + const handleFile = useCallback(async (file: File) => { + const preview = URL.createObjectURL(file) + setState({ status: 'uploading', file, preview }) + + try { + setState(s => ({ ...s, status: 'analysing' })) + const result = mediaType === 'image' + ? await detectImage(file) + : await detectVideo(file) + setState({ status: 'done', file, preview, result }) + } catch (err) { + setState(s => ({ + ...s, + status: 'error', + error: err instanceof Error ? err.message : 'Unknown error', + })) + } + }, [mediaType]) + + const reset = () => { + if (state.preview) URL.revokeObjectURL(state.preview) + setState({ status: 'idle' }) + } + + return ( + <> + + Analyse · GenAI-DeepDetect + + +
+ {/* Nav */} + + +
+ + {/* IDLE — upload zone */} + {state.status === 'idle' && ( + + + {/* Info panel */} +
+ {['FINGERPRINT', 'COHERENCE', 'SSTGNN'].map((engine, i) => ( + +
+ {engine} +
+
+ {engine === 'FINGERPRINT' && 'Frequency-domain artifact analysis via ViT-B/16'} + {engine === 'COHERENCE' && 'Lip-audio synchronisation scoring (video only)'} + {engine === 'SSTGNN' && 'Facial landmark trajectory graph attention (video only)'} +
+
+ ))} +
+
+ )} + + {/* UPLOADING / ANALYSING — loading orbit */} + {(state.status === 'uploading' || state.status === 'analysing') && ( + + )} + + {/* ERROR */} + {state.status === 'error' && ( + +
+ Error +
+

{state.error}

+ +
+ )} + + {/* DONE — results */} + {state.status === 'done' && state.result && ( +
+ {/* Header row */} + +
Results
+ +
+ + {/* Preview + stats */} + + {/* Preview */} +
+ {state.preview && ( + Uploaded media + )} + {!state.preview && ( + + PREVIEW + + )} +
+ +
+
+ + {/* Stats */} +
+ + + + {state.file && ( + + )} +
+
+ + {/* Engine breakdown */} + +
Engine breakdown
+ +
+ + {/* Explanation */} + +
+ Analysis detail +
+

+ {state.result.explanation} +

+
+
+ )} +
+
+ + ) +} + +function StatPanel({ + label, value, mono = false, +}: { label: string; value: string; mono?: boolean }) { + return ( +
+
+ {label} +
+
+ {value} +
+
+ ) +} diff --git a/frontend/pages/index.tsx b/frontend/pages/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..419ce23b23d84f3cfb37cefd37293303e3b18376 --- /dev/null +++ b/frontend/pages/index.tsx @@ -0,0 +1,206 @@ +import type { NextPage } from 'next'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import { motion } from 'framer-motion'; +import { + Image as ImageIcon, + Film, + Mic, + Scan, + Cpu, + BarChart2, + FileText, +} from 'lucide-react'; +import type { LucideIcon } from 'lucide-react'; +import { ORBITAL_NODES } from '@/lib/constants'; + +const ICONS: Record = { + Image: ImageIcon, + Film, + Mic, + Scan, + Cpu, + BarChart2, + FileText, +}; + +// Node positions at radius=140 — 7 nodes, 360/7 ≈ 51.4° apart, starting at top (90°) +function nodePosition(index: number, radius = 140) { + const angle = Math.PI / 2 - (index * 2 * Math.PI) / 7; + return { + x: Math.round(radius * Math.cos(angle)), + y: Math.round(-radius * Math.sin(angle)), // flip Y for CSS + }; +} + +export default function Landing(): JSX.Element { + const router = useRouter(); + + return ( + <> + + GenAI-DeepDetect — Forensic Detection System + + +
+ {/* Nav */} + + + {/* Hero */} +
+ {/* Background paths */} +
+ + + + + + + + +
+ + {/* Eyebrow */} + + Forensic Detection System + + + {/* Orbital diagram */} + + {/* Orbit rings */} + {[280, 200].map((size, i) => ( +
+ ))} + + {/* Center node */} +
+ + GENAI +
+ DETECT +
+
+ + {/* Orbital nodes */} + {ORBITAL_NODES.map((node, i) => { + const pos = nodePosition(i); + const Icon = ICONS[node.icon]; + return ( +
+
+ {Icon && ( + + )} +
+ + {node.label} + +
+ ); + })} + + + {/* Headline */} + + Detect AI-Generated +
+ Content +
+ + + Three independent engines analyse your media for + synthetic fingerprints, temporal inconsistencies, and + facial landmark anomalies. + + + {/* CTAs */} + + + + +
+ + {/* Footer */} +
+ + GENAI-DEEPDETECT · v1.0.0 + + + Fingerprint · Coherence · SSTGNN + +
+
+ + ); +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000000000000000000000000000000000000..33ad091d26d8a9dc95ebdf616e217d985ec215b8 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/styles/globals.css b/frontend/styles/globals.css new file mode 100644 index 0000000000000000000000000000000000000000..79175681f042071be70596b044251271176cf83b --- /dev/null +++ b/frontend/styles/globals.css @@ -0,0 +1,83 @@ +@import url('https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300&family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,300&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --c-bg: #0D0D0D; + --c-surface: #262626; + --c-border: #1A1A1A; + --c-border-hi: #2A2A2A; + --c-muted: #595958; + --c-secondary: #A6A6A6; + --c-primary: #F2F2F2; + --font-sans: 'DM Sans', sans-serif; + --font-mono: 'DM Mono', monospace; + + /* Semantic verdict colors */ + --c-fake-bg: rgba(239, 68, 68, 0.15); + --c-fake-bd: rgba(239, 68, 68, 0.30); + --c-fake-tx: #fca5a5; + --c-real-bg: rgba(34, 197, 94, 0.10); + --c-real-bd: rgba(34, 197, 94, 0.25); + --c-real-tx: #86efac; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, +body { + background: var(--c-bg); + color: var(--c-primary); + font-family: var(--font-sans); + font-size: 15px; + line-height: 1.7; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Scrollbar */ +::-webkit-scrollbar { width: 4px; } +::-webkit-scrollbar-track { background: var(--c-bg); } +::-webkit-scrollbar-thumb { background: var(--c-border-hi); border-radius: 2px; } + +/* Focus visible */ +:focus-visible { + outline: 1px solid var(--c-muted); + outline-offset: 2px; +} + +/* Mono utility */ +.font-mono { font-family: var(--font-mono); } + +/* Section label with trailing line */ +.section-label { + display: flex; + align-items: center; + gap: 12px; + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--c-muted); +} +.section-label::after { + content: ''; + flex: 1; + height: 1px; + background: #222; +} + +/* Card hover lift */ +.card-interactive { + transition: transform 200ms ease, border-color 200ms ease; +} +.card-interactive:hover { + transform: translateY(-1px); + border-color: rgba(255, 255, 255, 0.15); +} diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..861cf3433b66dc94f16f2c273a9f61aa5ce32efe --- /dev/null +++ b/frontend/tailwind.config.ts @@ -0,0 +1,51 @@ +import type { Config } from 'tailwindcss' + +const config: Config = { + content: [ + './pages/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + colors: { + brand: { + bg: '#0D0D0D', + surface: '#262626', + border: '#1A1A1A', + borderHi: '#2A2A2A', + muted: '#595958', + secondary: '#A6A6A6', + primary: '#F2F2F2', + }, + }, + fontFamily: { + sans: ['DM Sans', 'sans-serif'], + mono: ['DM Mono', 'monospace'], + }, + borderRadius: { + card: '14px', + panel: '8px', + zone: '10px', + badge: '20px', + }, + animation: { + 'spin-slow': 'spin 2s linear infinite', + 'fade-up': 'fadeUp 0.4s ease-out forwards', + 'pulse-ring':'pulseRing 2s ease-in-out infinite', + }, + keyframes: { + fadeUp: { + '0%': { opacity: '0', transform: 'translateY(12px)' }, + '100%': { opacity: '1', transform: 'translateY(0)' }, + }, + pulseRing: { + '0%, 100%': { opacity: '0.4', transform: 'scale(1)' }, + '50%': { opacity: '0.8', transform: 'scale(1.04)' }, + }, + }, + }, + }, + plugins: [], +} + +export default config diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..ed4fe5e6436d5ed853df90e1bd58173970ee6ce3 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "paths": { + "@/components/*": ["./components/*"], + "@/lib/*": ["./lib/*"], + "@/styles/*": ["./styles/*"], + "@/types/*": ["./types/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/frontend/types/detection.ts b/frontend/types/detection.ts new file mode 100644 index 0000000000000000000000000000000000000000..8acdc4f23e2d77d9c3c012ba78484b87f46afa9f --- /dev/null +++ b/frontend/types/detection.ts @@ -0,0 +1,43 @@ +// frontend/types/detection.ts +// Types aligned with src/types.py. + +export type Verdict = 'FAKE' | 'REAL' | 'UNKNOWN' + +export type GeneratorLabel = + | 'real' + | 'unknown_gan' + | 'stable_diffusion' + | 'midjourney' + | 'dall_e' + | 'flux' + | 'firefly' + | 'imagen' + | 'sora' + +export interface EngineResult { + engine: string + verdict: Verdict + confidence: number + attributed_generator?: GeneratorLabel | string | null + explanation: string + processing_time_ms: number +} + +export interface DetectionResponse { + verdict: Verdict + confidence: number + attributed_generator: GeneratorLabel | string + explanation: string + processing_time_ms: number + engine_breakdown: EngineResult[] +} + +export type MediaType = 'image' | 'video' + +export interface AnalysisState { + status: 'idle' | 'uploading' | 'analysing' | 'done' | 'error' + file?: File + preview?: string + result?: DetectionResponse + error?: string +} diff --git a/frontend/types/framer-motion.d.ts b/frontend/types/framer-motion.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..dfbccd5c55e371970a7ee9ff6def270a22535f17 --- /dev/null +++ b/frontend/types/framer-motion.d.ts @@ -0,0 +1 @@ +declare module 'framer-motion'; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..fb3f9f32757d7e66df555840ad3e2ed40bd9da29 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1268 @@ +{ + "name": "genai-deepdetect", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@upstash/context7-mcp": "^2.1.4" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz", + "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.28.0.tgz", + "integrity": "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@upstash/context7-mcp": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@upstash/context7-mcp/-/context7-mcp-2.1.4.tgz", + "integrity": "sha512-PZ8ZgA5/tOshHi/7bQxAhbwukJUjmJvFmBtciH7P5IG4WirLEPo+8ieQgCrvwgPkWG5btym9a70iNVTL3IiF3Q==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.25.1", + "@types/express": "^5.0.4", + "commander": "^14.0.0", + "express": "^5.1.0", + "jose": "^6.1.3", + "undici": "^6.6.3", + "zod": "^4.3.4" + }, + "bin": { + "context7-mcp": "dist/index.js" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz", + "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==", + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", + "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..57d33f35fa61904bdb5db0e9ee9262635341dee5 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@upstash/context7-mcp": "^2.1.4" + } +} diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000000000000000000000000000000000000..1ec258a914160955ee7ae2d003e2d5c42a42ca72 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +addopts = -p no:tmpdir -p no:cacheprovider --ignore=tests/.tmp +testpaths = tests +norecursedirs = .tmp tmp* pytest_tmp node_modules frontend tmp_everything_claude_code diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..e1a768b5b7be64f2bb33d475fb2e49cb015cd932 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,42 @@ +# API +fastapi>=0.111.0 +uvicorn[standard]>=0.29.0 +python-multipart>=0.0.9 +aiofiles>=23.2.1 +httpx>=0.27.0 +pydantic>=2.7.0 +python-dotenv>=1.0.1 + +# ML - fingerprint +transformers>=4.40.0 +timm>=1.0.0 +torch>=2.1.0 +torchvision>=0.16.0 + +# ML - coherence +# facenet-pytorch currently has limited support on newer Python versions. +facenet-pytorch>=2.5.3; python_version < "3.13" +mediapipe>=0.10.14 +opencv-python-headless>=4.9.0 + +# ML - sstgnn +torch-geometric>=2.5.0 +scipy>=1.13.0 + +# Explainability - Gemini +google-generativeai>=0.8.0 + +# HuggingFace +huggingface-hub>=0.23.0 + +# RunPod serverless handler +runpod>=1.6.0 + +# Continual learning +apscheduler>=3.10.4 + +# Utils +Pillow>=10.3.0 +numpy>=1.26.0; python_version < "3.13" +numpy>=2.0.0; python_version >= "3.13" +scikit-learn>=1.5.0 diff --git a/runpod_handler.py b/runpod_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..cea2a0b29c72a8742e52c6bead8dd8040fdef0ae --- /dev/null +++ b/runpod_handler.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +import base64 +import io +import os +import tempfile +import time + +from PIL import Image + +os.environ.setdefault("MODEL_CACHE_DIR", "/tmp/models") +os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") + +from src.engines.coherence.engine import CoherenceEngine +from src.engines.fingerprint.engine import FingerprintEngine +from src.engines.sstgnn.engine import SSTGNNEngine +from src.explainability.explainer import explain +from src.fusion.fuser import fuse + +_fp = FingerprintEngine() +_co = CoherenceEngine() +_st = SSTGNNEngine() + + +def _extract_frames(video_path: str) -> list: + try: + import cv2 + except Exception: + return [] + + cap = cv2.VideoCapture(video_path) + frames = [] + index = 0 + while True: + ret, frame = cap.read() + if not ret: + break + if index % 4 == 0: + frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + index += 1 + if len(frames) >= 300: + break + cap.release() + return frames + + +def handler(job: dict) -> dict: + inp = job.get("input", {}) + encoded = inp.get("data") or inp.get("image_b64") + if not encoded: + raise ValueError("Missing input.data (base64 payload)") + + raw = base64.b64decode(encoded) + media_type = str(inp.get("media_type", "image")).lower() + + t0 = time.perf_counter() + + if media_type == "image": + image = Image.open(io.BytesIO(raw)).convert("RGB") + fp = _fp.run(image) + co = _co.run(image) + st = _st.run(image) + verdict, conf, generator = fuse([fp, co, st], is_video=False) + else: + with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as temp: + temp.write(raw) + tmp_path = temp.name + + try: + frames = _extract_frames(tmp_path) + finally: + os.unlink(tmp_path) + + fp = _fp.run_video(frames) + co = _co.run_video(frames) + st = _st.run_video(frames) + verdict, conf, generator = fuse([fp, co, st], is_video=True) + + engine_results = [fp, co, st] + explanation = explain(verdict, conf, engine_results, generator) + total_ms = (time.perf_counter() - t0) * 1000 + + return { + "verdict": verdict, + "confidence": conf, + "attributed_generator": generator, + "explanation": explanation, + "processing_time_ms": total_ms, + "engine_breakdown": [result.model_dump() for result in engine_results], + } + + +try: + import runpod # type: ignore +except Exception: + runpod = None + + +if runpod is not None: + runpod.serverless.start({"handler": handler}) diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/scripts/assemble_image_baseline.py b/scripts/assemble_image_baseline.py new file mode 100644 index 0000000000000000000000000000000000000000..2f68cfbf9c5947ee7c378d1b6b6c03115c91477a --- /dev/null +++ b/scripts/assemble_image_baseline.py @@ -0,0 +1,37 @@ +""" +scripts/assemble_image_baseline.py + +Assembles the image baseline dataset manifest from multiple prepared source +directories. Delegates to build_dataset.py with the correct Kaggle input paths. + +Kaggle usage: + !python scripts/assemble_image_baseline.py \ + --faces_140k /kaggle/input/140k-real-and-fake-faces \ + --ai_vs_real /kaggle/input/ai-generated-vs-real-images-datasaet \ + --output_dir /kaggle/working/processed/fingerprint +""" +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) +from training.phase1_fingerprint.build_dataset import build, parse_args as _parse + + +def main() -> None: + p = argparse.ArgumentParser(description="Assemble image baseline dataset (Kaggle)") + p.add_argument("--faces_140k", default=None) + p.add_argument("--ai_vs_real", default=None) + p.add_argument("--deepfake_real", default=None) + p.add_argument("--deepfake_faces",default=None) + p.add_argument("--celebdf", default=None) + p.add_argument("--output_dir", default="/kaggle/working/processed/fingerprint") + p.add_argument("--seed", type=int, default=42) + args = p.parse_args() + build(args) + + +if __name__ == "__main__": + main() diff --git a/scripts/bootstrap_kaggle_dataset.py b/scripts/bootstrap_kaggle_dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..9f57979db0d240fea492df118aa9fdd1929cbee6 --- /dev/null +++ b/scripts/bootstrap_kaggle_dataset.py @@ -0,0 +1,72 @@ +""" +scripts/bootstrap_kaggle_dataset.py + +Uploads processed dataset to Kaggle as a private dataset so it can be +reused across notebook sessions without re-running preprocessing. + +Usage (run locally after downloading processed data from Kaggle): + python scripts/bootstrap_kaggle_dataset.py \ + --processed_dir /path/to/processed/fingerprint \ + --dataset_name deepdetect-fingerprint-processed \ + --title "DeepDetect Fingerprint Processed Dataset" +""" +from __future__ import annotations + +import argparse +import json +import os +import subprocess +import sys +from pathlib import Path + + +def main(args: argparse.Namespace) -> None: + processed_dir = Path(args.processed_dir) + dataset_name = args.dataset_name + kaggle_username = args.username or os.environ.get("KAGGLE_USERNAME") + + if not kaggle_username: + print("ERROR: Kaggle username required. Set --username or KAGGLE_USERNAME env var.") + sys.exit(1) + + if not processed_dir.exists(): + print(f"ERROR: Directory not found: {processed_dir}") + sys.exit(1) + + # Write dataset-metadata.json + meta = { + "title": args.title, + "id": f"{kaggle_username}/{dataset_name}", + "licenses": [{"name": "CC0-1.0"}], + } + meta_path = processed_dir / "dataset-metadata.json" + with open(meta_path, "w") as f: + json.dump(meta, f, indent=2) + + print(f"Creating Kaggle dataset: {kaggle_username}/{dataset_name}") + result = subprocess.run( + ["kaggle", "datasets", "create", "-p", str(processed_dir), "--dir-mode", "zip"], + capture_output=True, text=True, + ) + + if result.returncode == 0: + print(f"Dataset created: https://www.kaggle.com/datasets/{kaggle_username}/{dataset_name}") + else: + print(f"STDOUT: {result.stdout}") + print(f"STDERR: {result.stderr}") + print("If dataset already exists, use 'kaggle datasets version' to update it.") + + meta_path.unlink() # clean up + + +def parse_args(): + p = argparse.ArgumentParser(description="Upload processed dataset to Kaggle") + p.add_argument("--processed_dir", required=True) + p.add_argument("--dataset_name", required=True) + p.add_argument("--title", default="DeepDetect Processed Dataset") + p.add_argument("--username", default=None) + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/scripts/demo_smoke_test.py b/scripts/demo_smoke_test.py new file mode 100644 index 0000000000000000000000000000000000000000..ef4e542004f45c81931e1528e7e1c13aec8f4dc3 --- /dev/null +++ b/scripts/demo_smoke_test.py @@ -0,0 +1,298 @@ +""" +scripts/demo_smoke_test.py + +Prepare demo sample media and run one-command smoke tests against the running API. + +Usage: + python scripts/demo_smoke_test.py + python scripts/demo_smoke_test.py --base-url http://localhost:8000 + +Environment: + DEMO_API_BASE_URL overrides default base URL. +""" + +from __future__ import annotations + +import argparse +import logging +import os +import sys +from pathlib import Path +from typing import Any, cast + +import httpx +import numpy as np +from PIL import Image, ImageDraw + + +LOG_FORMAT = "%(levelname)s: %(message)s" +logging.basicConfig(level=logging.INFO, format=LOG_FORMAT) +log = logging.getLogger(__name__) + + +SAMPLE_DIR = Path("data/samples") +REQUIRED_RESPONSE_KEYS = { + "verdict", + "confidence", + "attributed_generator", + "explanation", + "processing_time_ms", + "engine_breakdown", +} + + +def _download_file(url: str, output: Path) -> bool: + try: + with httpx.Client(timeout=20.0, follow_redirects=True) as client: + resp = client.get(url) + resp.raise_for_status() + output.write_bytes(resp.content) + return True + except Exception as exc: + log.warning("Could not download %s (%s)", url, exc) + return False + + +def _make_fallback_fake_image(fake_img: Path) -> None: + # Synthetic-looking sample (deterministic). + fake = Image.new("RGB", (512, 512), (12, 12, 12)) + draw_fake = ImageDraw.Draw(fake) + for i in range(0, 512, 16): + color = (40 + (i % 120), 40, 120 + (i % 80)) + draw_fake.rectangle([i, 0, i + 8, 511], fill=color) + draw_fake.ellipse([140, 110, 380, 350], outline=(220, 220, 220), width=4) + draw_fake.text((168, 380), "SYNTHETIC SAMPLE", fill=(220, 220, 220)) + fake.save(fake_img, format="JPEG", quality=95) + + +def _make_fallback_real_image(real_img: Path) -> None: + # Natural-looking simple fallback when a real photo cannot be downloaded. + arr = np.zeros((512, 512, 3), dtype=np.uint8) + for y in range(512): + arr[y, :, 0] = np.uint8(200 - y // 4) + arr[y, :, 1] = np.uint8(160 - y // 6) + arr[y, :, 2] = np.uint8(140 - y // 8) + real = Image.fromarray(arr, mode="RGB") + draw_real = ImageDraw.Draw(real) + draw_real.rectangle([56, 280, 456, 508], outline=(245, 245, 245), width=2) + draw_real.text((70, 468), "REAL-LIKE SAMPLE", fill=(245, 245, 245)) + real.save(real_img, format="JPEG", quality=95) + + +def _prepare_images( + sample_dir: Path, + refresh: bool = False, + use_web_samples: bool = False, +) -> tuple[Path, Path]: + sample_dir.mkdir(parents=True, exist_ok=True) + fake_img = sample_dir / "fake_1.jpg" + real_img = sample_dir / "real_1.jpg" + + if not refresh and fake_img.exists() and real_img.exists(): + return fake_img, real_img + + fake_ok = False + if use_web_samples: + fake_ok = _download_file("https://thispersondoesnotexist.com", fake_img) + if not fake_ok: + _make_fallback_fake_image(fake_img) + + real_ok = _download_file( + "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=512", + real_img, + ) + if not real_ok: + log.info("Could not download real sample image; using generated fallback real image.") + _make_fallback_real_image(real_img) + + return fake_img, real_img + + +def _prepare_video(sample_dir: Path) -> Path: + import cv2 + + sample_dir.mkdir(parents=True, exist_ok=True) + video_path = sample_dir / "demo_1.avi" + if video_path.exists() and video_path.stat().st_size > 0: + return video_path + + width, height = 512, 512 + fps = 10.0 + frames = 40 + + fourcc_fn = cast(Any, getattr(cv2, "VideoWriter_fourcc", None)) + if not callable(fourcc_fn): + raise RuntimeError("OpenCV VideoWriter_fourcc is unavailable") + fourcc_xvid = cast(int, fourcc_fn(*"XVID")) + fourcc_mjpg = cast(int, fourcc_fn(*"MJPG")) + + writer = cv2.VideoWriter( + str(video_path), fourcc_xvid, fps, (width, height) + ) + if not writer.isOpened(): + writer = cv2.VideoWriter( + str(video_path), fourcc_mjpg, fps, (width, height) + ) + + if not writer.isOpened(): + raise RuntimeError("Could not initialize AVI video writer") + + for i in range(frames): + frame = np.zeros((height, width, 3), dtype=np.uint8) + # Background gradient animation + frame[:, :, 0] = np.uint8((40 + i * 3) % 255) + frame[:, :, 1] = np.uint8((20 + i * 2) % 255) + frame[:, :, 2] = np.uint8((60 + i * 4) % 255) + + # Moving face-like circle + eyes/mouth + cx = int(120 + (i * 8) % 280) + cy = 220 + cv2.circle(frame, (cx, cy), 70, (220, 220, 220), 2) + cv2.circle(frame, (cx - 24, cy - 12), 6, (230, 230, 230), -1) + cv2.circle(frame, (cx + 24, cy - 12), 6, (230, 230, 230), -1) + cv2.ellipse(frame, (cx, cy + 20), (24, 12), 0, 0, 180, (230, 230, 230), 2) + + cv2.putText( + frame, + "DEMO VIDEO SAMPLE", + (110, 470), + cv2.FONT_HERSHEY_SIMPLEX, + 0.8, + (245, 245, 245), + 2, + cv2.LINE_AA, + ) + writer.write(frame) + + writer.release() + + if not video_path.exists() or video_path.stat().st_size == 0: + raise RuntimeError("Video file was not written") + + return video_path + + +def _validate_response(payload: dict) -> None: + missing = REQUIRED_RESPONSE_KEYS - set(payload.keys()) + if missing: + raise ValueError(f"Response missing keys: {sorted(missing)}") + + verdict = payload.get("verdict") + confidence = payload.get("confidence") + + if verdict not in {"FAKE", "REAL"}: + raise ValueError(f"Invalid verdict: {verdict}") + if not isinstance(confidence, (int, float)): + raise ValueError("confidence is not numeric") + if confidence < 0.0 or confidence > 1.0: + raise ValueError(f"confidence out of range: {confidence}") + + +def _post_file(base_url: str, endpoint: str, path: Path, content_type: str) -> dict: + with httpx.Client(timeout=60.0) as client: + with path.open("rb") as fh: + files = {"file": (path.name, fh, content_type)} + resp = client.post(f"{base_url}{endpoint}", files=files) + + if resp.status_code != 200: + raise RuntimeError( + f"{endpoint} returned {resp.status_code}: {resp.text[:300]}" + ) + + payload = resp.json() + _validate_response(payload) + return payload + + +def _check_health(base_url: str) -> None: + with httpx.Client(timeout=15.0) as client: + resp = client.get(f"{base_url}/health") + resp.raise_for_status() + + +def run_smoke( + base_url: str, + refresh_samples: bool = False, + use_web_samples: bool = False, +) -> int: + base_url = base_url.rstrip("/") + log.info("Using API base URL: %s", base_url) + + _check_health(base_url) + log.info("Health check OK") + + fake_img, real_img = _prepare_images( + SAMPLE_DIR, + refresh=refresh_samples, + use_web_samples=use_web_samples, + ) + video = _prepare_video(SAMPLE_DIR) + + image_fake = _post_file(base_url, "/detect/image", fake_img, "image/jpeg") + image_real = _post_file(base_url, "/detect/image", real_img, "image/jpeg") + video_res = _post_file(base_url, "/detect/video", video, "video/x-msvideo") + + log.info( + "Image test 1 (%s): verdict=%s confidence=%.3f generator=%s", + fake_img.name, + image_fake["verdict"], + float(image_fake["confidence"]), + image_fake["attributed_generator"], + ) + log.info( + "Image test 2 (%s): verdict=%s confidence=%.3f generator=%s", + real_img.name, + image_real["verdict"], + float(image_real["confidence"]), + image_real["attributed_generator"], + ) + log.info( + "Video test (%s): verdict=%s confidence=%.3f generator=%s", + video.name, + video_res["verdict"], + float(video_res["confidence"]), + video_res["attributed_generator"], + ) + + log.info("Smoke test PASSED") + return 0 + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Prepare demo media and smoke-test image/video endpoints" + ) + parser.add_argument( + "--base-url", + default=os.environ.get("DEMO_API_BASE_URL", "http://localhost:8000"), + help="API base URL (default: %(default)s)", + ) + parser.add_argument( + "--refresh-samples", + action="store_true", + help="Re-download/regenerate demo samples instead of reusing existing files", + ) + parser.add_argument( + "--use-web-samples", + action="store_true", + help="Use downloaded web sample for the fake image (less deterministic)", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + base_url = args.base_url + try: + return run_smoke( + base_url, + refresh_samples=args.refresh_samples, + use_web_samples=args.use_web_samples, + ) + except Exception as exc: + log.error("Smoke test FAILED: %s", exc) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/kaggle_train_image_baseline.py b/scripts/kaggle_train_image_baseline.py new file mode 100644 index 0000000000000000000000000000000000000000..b2480b72a72e3782bb70f1472b92771468344d97 --- /dev/null +++ b/scripts/kaggle_train_image_baseline.py @@ -0,0 +1,235 @@ +""" +scripts/kaggle_train_image_baseline.py + +Self-contained Kaggle training script. +Paste directly into a Kaggle notebook code cell and run. +No separate data prep step needed — builds the dataset inline from Kaggle input mounts. + +Datasets needed (add via Notebook → Add Data): + - xhlulu/140k-real-and-fake-faces + - philosopher0/ai-generated-vs-real-images-datasaet + +Accelerator: P100 (16GB VRAM) +""" +from __future__ import annotations + +import os, sys, logging, random, time, csv, shutil, hashlib +from pathlib import Path + +import numpy as np +import torch +import torch.nn as nn +from torch.cuda.amp import GradScaler, autocast +from torch.utils.data import DataLoader, Dataset, WeightedRandomSampler +from torchvision import transforms +from PIL import Image +from sklearn.metrics import roc_auc_score, accuracy_score +from tqdm import tqdm + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") +log = logging.getLogger(__name__) + +# ── Configuration — edit these ──────────────────────────────────────────────── + +CFG = { + "faces_140k": "/kaggle/input/140k-real-and-fake-faces", + "ai_vs_real": "/kaggle/input/ai-generated-vs-real-images-datasaet", + "processed": "/kaggle/working/processed/fingerprint", + "output": "/kaggle/working/checkpoints/fingerprint", + "epochs": 5, # 5 for demo (~55 min), 30 for full run (~6h) + "batch_size": 64, + "lr": 2e-5, + "amp": True, + "seed": 42, + "workers": 2, +} + +torch.manual_seed(CFG["seed"]) +np.random.seed(CFG["seed"]) +random.seed(CFG["seed"]) + +Path(CFG["processed"]).mkdir(parents=True, exist_ok=True) +Path(CFG["output"]).mkdir(parents=True, exist_ok=True) + +# ── GPU check ──────────────────────────────────────────────────────────────── + +print(f"PyTorch {torch.__version__} | CUDA {torch.cuda.is_available()}") +if torch.cuda.is_available(): + print(f"GPU: {torch.cuda.get_device_name(0)}") +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +# ── Dataset build ───────────────────────────────────────────────────────────── + +IMG_EXTS = {".jpg", ".jpeg", ".png", ".webp"} + +def collect_images(root, label_str): + records = [] + for p in Path(root).rglob("*"): + if p.suffix.lower() in IMG_EXTS: + records.append((p, label_str)) + return records + +def build_dataset(): + log.info("Building fingerprint dataset ...") + all_records = [] + + # 140k faces: has train/real and train/fake folders + if Path(CFG["faces_140k"]).exists(): + for split in ["train", "valid", ""]: + for label in ["real", "fake"]: + d = Path(CFG["faces_140k"]) / split / label if split else Path(CFG["faces_140k"]) / label + if d.exists(): + imgs = collect_images(d, label) + all_records.extend(imgs) + log.info(f" 140k/{split or 'root'}/{label}: {len(imgs)}") + + # AI vs real: subfolder name = generator + if Path(CFG["ai_vs_real"]).exists(): + for sub in Path(CFG["ai_vs_real"]).iterdir(): + if not sub.is_dir(): continue + label = "real" if "real" in sub.name.lower() else "fake" + imgs = collect_images(sub, label) + all_records.extend(imgs) + log.info(f" ai_vs_real/{sub.name} → {label}: {len(imgs)}") + + random.shuffle(all_records) + n = len(all_records) + n_train = int(n * 0.80) + n_val = int(n * 0.10) + splits = { + "train": all_records[:n_train], + "val": all_records[n_train:n_train+n_val], + "test": all_records[n_train+n_val:], + } + + seen = set() + for split, records in splits.items(): + for src, label in tqdm(records, desc=f"Copying {split}"): + h = hashlib.md5(src.read_bytes()).hexdigest()[:8] + dst = Path(CFG["processed"]) / split / label / f"{h}_{src.name}" + if h not in seen and not dst.exists(): + dst.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(src, dst) + seen.add(h) + + for split in ["train", "val"]: + for label in ["real", "fake"]: + count = len(list((Path(CFG["processed"]) / split / label).glob("*"))) + log.info(f" {split}/{label}: {count}") + +build_dataset() + +# ── Transforms ──────────────────────────────────────────────────────────────── + +MEAN, STD = [0.485, 0.456, 0.406], [0.229, 0.224, 0.225] +TRAIN_TF = transforms.Compose([ + transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), + transforms.RandomHorizontalFlip(), + transforms.ColorJitter(0.2, 0.2, 0.2, 0.1), + transforms.ToTensor(), + transforms.Normalize(MEAN, STD), +]) +VAL_TF = transforms.Compose([ + transforms.Resize(256), transforms.CenterCrop(224), + transforms.ToTensor(), transforms.Normalize(MEAN, STD), +]) + +# ── Dataset class ────────────────────────────────────────────────────────────── + +class ImgDataset(Dataset): + def __init__(self, root, split, transform): + self.transform = transform + self.samples = [] + for label, is_fake in [("real", 0), ("fake", 1)]: + d = Path(root) / split / label + if not d.exists(): continue + for p in d.iterdir(): + if p.suffix.lower() in IMG_EXTS: + self.samples.append((p, is_fake)) + log.info(f"Dataset [{split}]: {len(self.samples)} images") + + def __len__(self): return len(self.samples) + def __getitem__(self, i): + p, label = self.samples[i] + img = Image.open(p).convert("RGB") + return self.transform(img), label + +train_ds = ImgDataset(CFG["processed"], "train", TRAIN_TF) +val_ds = ImgDataset(CFG["processed"], "val", VAL_TF) +labels = [s[1] for s in train_ds.samples] +n_real = labels.count(0); n_fake = labels.count(1) +weights = [1.0/n_real if l==0 else 1.0/n_fake for l in labels] +sampler = WeightedRandomSampler(weights, len(weights), replacement=True) +train_dl = DataLoader(train_ds, batch_size=CFG["batch_size"], sampler=sampler, + num_workers=CFG["workers"], pin_memory=True) +val_dl = DataLoader(val_ds, batch_size=CFG["batch_size"]*2, shuffle=False, + num_workers=CFG["workers"], pin_memory=True) + +# ── Model ───────────────────────────────────────────────────────────────────── + +import timm +class FingerprintModel(nn.Module): + def __init__(self): + super().__init__() + self.backbone = timm.create_model("vit_base_patch16_224", pretrained=True, + num_classes=0, global_pool="token") + d = self.backbone.num_features + self.binary_head = nn.Sequential(nn.Linear(d, 384), nn.GELU(), nn.Dropout(0.1), nn.Linear(384, 2)) + self.generator_head = nn.Sequential(nn.Linear(d, 384), nn.GELU(), nn.Dropout(0.1), nn.Linear(384, 8)) + + def forward(self, x): + f = self.backbone(x) + return {"binary_logits": self.binary_head(f), "generator_logits": self.generator_head(f)} + +model = FingerprintModel().to(device) +cw = torch.tensor([1.0/n_real, 1.0/n_fake], dtype=torch.float32).to(device) +criterion = nn.CrossEntropyLoss(weight=cw, label_smoothing=0.05) +optimizer = torch.optim.AdamW(model.parameters(), lr=CFG["lr"], weight_decay=0.01) +scaler = GradScaler(enabled=CFG["amp"]) + +# ── Training loop ───────────────────────────────────────────────────────────── + +best_auc = 0.0 +for epoch in range(CFG["epochs"]): + model.train() + t0 = time.time() + for imgs, labels_b in tqdm(train_dl, desc=f"Train E{epoch+1}", leave=False): + imgs, labels_b = imgs.to(device), labels_b.to(device) + with autocast(enabled=CFG["amp"]): + out = model(imgs) + loss = criterion(out["binary_logits"], labels_b) + scaler.scale(loss).backward() + scaler.unscale_(optimizer) + nn.utils.clip_grad_norm_(model.parameters(), 1.0) + scaler.step(optimizer); scaler.update(); optimizer.zero_grad() + + # Validate + model.eval() + all_probs, all_labels = [], [] + with torch.no_grad(): + for imgs, labels_b in tqdm(val_dl, desc="Val", leave=False): + imgs = imgs.to(device) + out = model(imgs) + probs = torch.softmax(out["binary_logits"], 1)[:, 1].cpu().numpy() + all_probs.extend(probs); all_labels.extend(labels_b.numpy()) + + auc = roc_auc_score(all_labels, all_probs) if len(set(all_labels)) > 1 else 0.5 + acc = accuracy_score(all_labels, (np.array(all_probs) > 0.5).astype(int)) + elapsed = time.time() - t0 + log.info(f"Epoch {epoch+1}/{CFG['epochs']} | val_auc={auc:.4f} | val_acc={acc:.4f} | {elapsed:.0f}s") + + if auc > best_auc: + best_auc = auc + ckpt = { + "epoch": epoch, "model_state": model.state_dict(), + "val_auc": auc, "config": CFG, + } + torch.save(ckpt, Path(CFG["output"]) / "best.pt") + log.info(f" Saved best.pt (AUC={best_auc:.4f})") + + torch.save({"epoch": epoch, "model_state": model.state_dict(), + "val_auc": auc, "config": CFG}, + Path(CFG["output"]) / "latest.pt") + +print(f"\nTraining complete. Best val AUC: {best_auc:.4f}") +print(f"Download: /kaggle/working/checkpoints/fingerprint/best.pt") diff --git a/scripts/prepare_cifake.py b/scripts/prepare_cifake.py new file mode 100644 index 0000000000000000000000000000000000000000..d2bda896caf553a5a3a31520e992763b2a296862 --- /dev/null +++ b/scripts/prepare_cifake.py @@ -0,0 +1,76 @@ +""" +scripts/prepare_cifake.py + +Prepares the CIFAKE dataset (Kaggle: bird-coder/cifake-real-and-ai-generated-synthetic-images) +for use in the fingerprint engine training pipeline. + +CIFAKE contains 60k real images (CIFAR-10) and 60k AI-generated equivalents. +Useful as extra training data for the fingerprint engine. + +Kaggle usage: + !python scripts/prepare_cifake.py \ + --source /kaggle/input/cifake-real-and-ai-generated-synthetic-images \ + --output /kaggle/working/processed/fingerprint \ + --max_per_class 20000 +""" +from __future__ import annotations + +import argparse +import logging +import random +import shutil +from pathlib import Path + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") +log = logging.getLogger(__name__) + +IMG_EXTS = {".jpg", ".jpeg", ".png"} + + +def main(args: argparse.Namespace) -> None: + source = Path(args.source) + output = Path(args.output) + rng = random.Random(args.seed) + + if not source.exists(): + log.error(f"Source not found: {source}") + return + + for split in ["train", "test"]: + for label, is_fake in [("REAL", "real"), ("FAKE", "fake")]: + src_dir = source / split / label + if not src_dir.exists(): + src_dir = source / label + if not src_dir.exists(): + log.warning(f" Not found: {src_dir}") + continue + + imgs = [p for p in src_dir.iterdir() if p.suffix.lower() in IMG_EXTS] + rng.shuffle(imgs) + imgs = imgs[:args.max_per_class] + + out_split = "train" if split == "train" else "val" + dst_dir = output / out_split / is_fake + dst_dir.mkdir(parents=True, exist_ok=True) + + for img in imgs: + dst = dst_dir / f"cifake_{img.name}" + if not dst.exists(): + shutil.copy2(img, dst) + + log.info(f" cifake/{split}/{label} → {out_split}/{is_fake}: {len(imgs)} images") + + log.info("CIFAKE preparation complete.") + + +def parse_args(): + p = argparse.ArgumentParser() + p.add_argument("--source", default="/kaggle/input/cifake-real-and-ai-generated-synthetic-images") + p.add_argument("--output", default="/kaggle/working/processed/fingerprint") + p.add_argument("--max_per_class", type=int, default=20000) + p.add_argument("--seed", type=int, default=42) + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/scripts/prepare_cocoxgen.py b/scripts/prepare_cocoxgen.py new file mode 100644 index 0000000000000000000000000000000000000000..64e4b791afc36118f5c9045a64ed41d1557b36b4 --- /dev/null +++ b/scripts/prepare_cocoxgen.py @@ -0,0 +1,76 @@ +""" +scripts/prepare_cocoxgen.py + +Prepares COCO-XGen dataset for fingerprint engine training. +Maps COCO real images against XL-generated equivalents for generator attribution training. + +Kaggle usage: + !python scripts/prepare_cocoxgen.py \ + --real_source /kaggle/input/coco-2017-dataset/coco2017/val2017 \ + --fake_source /kaggle/input/coco-xgen-synthetic \ + --output /kaggle/working/processed/fingerprint \ + --max 15000 +""" +from __future__ import annotations + +import argparse +import logging +import random +import shutil +from pathlib import Path + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") +log = logging.getLogger(__name__) + +IMG_EXTS = {".jpg", ".jpeg", ".png"} + + +def copy_subset(src_dir: Path, dst_dir: Path, max_n: int, prefix: str, rng: random.Random) -> int: + dst_dir.mkdir(parents=True, exist_ok=True) + imgs = [p for p in src_dir.rglob("*") if p.suffix.lower() in IMG_EXTS] + rng.shuffle(imgs) + imgs = imgs[:max_n] + for img in imgs: + dst = dst_dir / f"{prefix}_{img.name}" + if not dst.exists(): + shutil.copy2(img, dst) + return len(imgs) + + +def main(args: argparse.Namespace) -> None: + rng = random.Random(args.seed) + + splits = {"train": int(args.max * 0.9), "val": int(args.max * 0.1)} + + for split, max_n in splits.items(): + if args.real_source and Path(args.real_source).exists(): + n = copy_subset( + Path(args.real_source), + Path(args.output) / split / "real", + max_n, "coco", rng + ) + log.info(f" real/{split}: {n} images") + + if args.fake_source and Path(args.fake_source).exists(): + n = copy_subset( + Path(args.fake_source), + Path(args.output) / split / "fake", + max_n, "xgen", rng + ) + log.info(f" fake/{split}: {n} images (generator: stable_diffusion/xl)") + + log.info("COCO-XGen preparation complete.") + + +def parse_args(): + p = argparse.ArgumentParser() + p.add_argument("--real_source", default=None) + p.add_argument("--fake_source", default=None) + p.add_argument("--output", default="/kaggle/working/processed/fingerprint") + p.add_argument("--max", type=int, default=15000) + p.add_argument("--seed", type=int, default=42) + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/scripts/prepare_hf_source.py b/scripts/prepare_hf_source.py new file mode 100644 index 0000000000000000000000000000000000000000..e12df356fe66a1bf722692ea2feeae455f603353 --- /dev/null +++ b/scripts/prepare_hf_source.py @@ -0,0 +1,79 @@ +""" +scripts/prepare_hf_source.py + +Downloads and prepares datasets from HuggingFace Hub for fingerprint engine training. +Requires: pip install datasets huggingface_hub + +Kaggle usage (internet must be enabled in notebook settings): + !pip install datasets huggingface_hub -q + !python scripts/prepare_hf_source.py \ + --dataset elsaEU/ELSA_1M \ + --split train \ + --output /kaggle/working/processed/fingerprint \ + --max 10000 +""" +from __future__ import annotations + +import argparse +import logging +from pathlib import Path + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") +log = logging.getLogger(__name__) + + +def main(args: argparse.Namespace) -> None: + try: + from datasets import load_dataset + except ImportError: + log.error("datasets not installed. Run: pip install datasets") + return + + log.info(f"Loading {args.dataset} (split={args.split}, max={args.max}) ...") + + try: + ds = load_dataset(args.dataset, split=args.split, streaming=True) + except Exception as e: + log.error(f"Failed to load dataset: {e}") + return + + output = Path(args.output) + real_dir = output / "train" / "real" + fake_dir = output / "train" / "fake" + real_dir.mkdir(parents=True, exist_ok=True) + fake_dir.mkdir(parents=True, exist_ok=True) + + n_saved = 0 + for i, sample in enumerate(ds): + if n_saved >= args.max: + break + + img = sample.get("image") or sample.get("img") + label = sample.get("label", sample.get("is_fake", 0)) + + if img is None: + continue + + dst_dir = fake_dir if label else real_dir + dst = dst_dir / f"hf_{i:07d}.jpg" + if not dst.exists(): + img.save(dst, format="JPEG", quality=95) + n_saved += 1 + + if n_saved % 1000 == 0: + log.info(f" Saved {n_saved}/{args.max} ...") + + log.info(f"HuggingFace source preparation complete. {n_saved} images saved.") + + +def parse_args(): + p = argparse.ArgumentParser() + p.add_argument("--dataset", default="elsaEU/ELSA_1M") + p.add_argument("--split", default="train") + p.add_argument("--output", default="/kaggle/working/processed/fingerprint") + p.add_argument("--max", type=int, default=10000) + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/scripts/prepare_ifakefakedb.py b/scripts/prepare_ifakefakedb.py new file mode 100644 index 0000000000000000000000000000000000000000..5f2ef254539cd7b893a1b25e9f2ee57071bba18d --- /dev/null +++ b/scripts/prepare_ifakefakedb.py @@ -0,0 +1,66 @@ +""" +scripts/prepare_ifakefakedb.py + +Prepares the iFakeFaceDB dataset for fingerprint engine training. +Kaggle slug: tapakah68/artificial-faces-dataset or similar. + +iFakeFaceDB contains ~87k StyleGAN-generated fake faces, useful for +increasing unknown_gan class coverage. + +Kaggle usage: + !python scripts/prepare_ifakefakedb.py \ + --source /kaggle/input/artificial-faces-dataset \ + --output /kaggle/working/processed/fingerprint \ + --max 20000 +""" +from __future__ import annotations + +import argparse +import logging +import random +import shutil +from pathlib import Path + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") +log = logging.getLogger(__name__) + +IMG_EXTS = {".jpg", ".jpeg", ".png"} + + +def main(args: argparse.Namespace) -> None: + source = Path(args.source) + if not source.exists(): + log.error(f"Source not found: {source}") + return + + rng = random.Random(args.seed) + imgs = [p for p in source.rglob("*") if p.suffix.lower() in IMG_EXTS] + rng.shuffle(imgs) + imgs = imgs[:args.max] + + n_train = int(len(imgs) * 0.85) + splits = {"train": imgs[:n_train], "val": imgs[n_train:]} + + for split, subset in splits.items(): + dst_dir = Path(args.output) / split / "fake" + dst_dir.mkdir(parents=True, exist_ok=True) + for img in subset: + dst = dst_dir / f"ifake_{img.name}" + if not dst.exists(): + shutil.copy2(img, dst) + log.info(f" {split}/fake: {len(subset)} images (generator: unknown_gan / StyleGAN)") + + log.info("iFakeFaceDB preparation complete.") + + +def parse_args(): + p = argparse.ArgumentParser() + p.add_argument("--source", default="/kaggle/input/artificial-faces-dataset") + p.add_argument("--output", default="/kaggle/working/processed/fingerprint") + p.add_argument("--max", type=int, default=20000) + p.add_argument("--seed", type=int, default=42) + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/scripts/render_kaggle_config.py b/scripts/render_kaggle_config.py new file mode 100644 index 0000000000000000000000000000000000000000..5cb5b6df3b9f719b8c244e57cc5acaeb3ef45d8a --- /dev/null +++ b/scripts/render_kaggle_config.py @@ -0,0 +1,64 @@ +""" +scripts/render_kaggle_config.py + +Generates a ready-to-paste Kaggle notebook JSON config from a YAML training config. +Useful for reproducible Kaggle runs. + +Usage: + python scripts/render_kaggle_config.py --config configs/fingerprint_baseline.yaml +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +import yaml + + +def main(args: argparse.Namespace) -> None: + config_path = Path(args.config) + with open(config_path) as f: + cfg = yaml.safe_load(f) + + engine = cfg.get("engine", "fingerprint") + + notebook_config = { + "id": f"akagtag/deepdetect-{engine}-training", + "language": "python", + "kernel_type": "notebook", + "is_private": True, + "enable_gpu": True, + "enable_tpu": False, + "enable_internet": True, + "dataset_sources": cfg.get("kaggle_datasets", []), + "competition_sources": cfg.get("kaggle_competitions", []), + "kernel_sources": [], + } + + out_path = Path(f".kaggle-kernel/kernel-metadata-{engine}.json") + out_path.parent.mkdir(parents=True, exist_ok=True) + with open(out_path, "w") as f: + json.dump(notebook_config, f, indent=2) + + print(f"Kaggle notebook config written to {out_path}") + print(f"\nTraining args from config:") + train_args = cfg.get("train_args", {}) + for k, v in train_args.items(): + print(f" --{k} {v}") + + cmd = "python scripts/train_image_baseline.py" if engine == "fingerprint" else f"python training/phase{{'fingerprint':1,'coherence':2,'sstgnn':3}.get(engine,1)}_{engine}/train.py" + flag_str = " ".join(f"--{k} {v}" for k, v in train_args.items() if not isinstance(v, bool)) + bool_flags = " ".join(f"--{k}" for k, v in train_args.items() if v is True) + print(f"\nKaggle cell command:") + print(f" !{cmd} {flag_str} {bool_flags}") + + +def parse_args(): + p = argparse.ArgumentParser() + p.add_argument("--config", default="configs/fingerprint_baseline.yaml") + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/scripts/train_image_baseline.py b/scripts/train_image_baseline.py new file mode 100644 index 0000000000000000000000000000000000000000..7becbf2a5e31b11ff4e08ad6c1a02fc7b3ebd8da --- /dev/null +++ b/scripts/train_image_baseline.py @@ -0,0 +1,130 @@ +""" +scripts/train_image_baseline.py + +PRIMARY Kaggle training entrypoint for the Fingerprint engine. +This is the script called in KAGGLE.md Phase 1 training cells. + +It is a thin wrapper around training/phase1_fingerprint/train.py that: + - Accepts the same CLI args + - Sets is_kaggle=True when /kaggle/working is detected + - Handles Kaggle-specific path resolution + - Prints a clear startup summary + +Kaggle usage: + !python scripts/train_image_baseline.py \ + --data_dir /kaggle/working/processed/fingerprint \ + --output_dir /kaggle/working/checkpoints/fingerprint \ + --epochs 5 --batch_size 64 --lr 2e-5 --amp --seed 42 +""" + +from __future__ import annotations + +import argparse +import logging +import os +import sys +from pathlib import Path + +# Repo root on sys.path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s %(message)s", + datefmt="%H:%M:%S", +) +log = logging.getLogger(__name__) + + +def detect_kaggle() -> bool: + return Path("/kaggle/working").exists() + + +def print_startup_summary(args: argparse.Namespace, is_kaggle: bool) -> None: + import torch + print("=" * 60) + print(" GenAI-DeepDetect — Fingerprint Engine Training") + print("=" * 60) + print(f" Environment : {'Kaggle' if is_kaggle else 'Local'}") + print(f" PyTorch : {torch.__version__}") + print(f" CUDA : {torch.cuda.is_available()}") + if torch.cuda.is_available(): + print(f" GPU : {torch.cuda.get_device_name(0)}") + print(f" VRAM : {torch.cuda.get_device_properties(0).total_memory // (1024**3)}GB") + print(f" Data dir : {args.data_dir}") + print(f" Output dir : {args.output_dir}") + print(f" Epochs : {args.epochs}") + print(f" Batch size : {args.batch_size}") + print(f" LR : {args.lr}") + print(f" AMP : {args.amp}") + print(f" Seed : {args.seed}") + print("=" * 60) + + # Estimate time + epochs = args.epochs + batch_size = args.batch_size + est_minutes = int(epochs * (180000 / batch_size) / 60 / 10) # rough 140k dataset + print(f" Est. time : ~{est_minutes} min on P100 for {epochs} epochs") + print("=" * 60) + + +def verify_data_dir(data_dir: str) -> bool: + """Check the dataset directory looks like it was built correctly.""" + root = Path(data_dir) + ok = True + for split in ["train", "val"]: + for label in ["real", "fake"]: + d = root / split / label + if not d.exists(): + log.warning(f"Missing: {d}") + ok = False + else: + count = len(list(d.glob("*.jpg")) + list(d.glob("*.png"))) + log.info(f" {split}/{label}: {count} images") + return ok + + +def main() -> None: + p = argparse.ArgumentParser(description="Train Fingerprint engine (Kaggle entrypoint)") + p.add_argument("--data_dir", required=True) + p.add_argument("--output_dir", required=True) + p.add_argument("--epochs", type=int, default=30) + p.add_argument("--batch_size", type=int, default=64) + p.add_argument("--lr", type=float, default=2e-5) + p.add_argument("--weight_decay", type=float, default=0.01) + p.add_argument("--warmup_steps", type=int, default=500) + p.add_argument("--grad_accum", type=int, default=1) + p.add_argument("--workers", type=int, default=4) + p.add_argument("--patience", type=int, default=5) + p.add_argument("--seed", type=int, default=42) + p.add_argument("--amp", action="store_true", default=False) + p.add_argument("--wandb", action="store_true", default=False) + p.add_argument("--resume", default=None) + p.add_argument("--skip_verify", action="store_true", default=False, + help="Skip dataset verification (faster startup on Kaggle)") + args = p.parse_args() + + is_kaggle = detect_kaggle() + if is_kaggle: + # On Kaggle: more workers may cause issues + args.workers = min(args.workers, 2) + + print_startup_summary(args, is_kaggle) + + # Verify dataset exists + if not args.skip_verify: + log.info("Verifying dataset ...") + if not verify_data_dir(args.data_dir): + log.error( + f"Dataset incomplete at {args.data_dir}. " + "Run build_dataset.py first (see KAGGLE.md Phase 1 Cell 5)." + ) + sys.exit(1) + + # Delegate to the full training module + from training.phase1_fingerprint.train import main as train_main + train_main(args) + + +if __name__ == "__main__": + main() diff --git a/spaces/README.md b/spaces/README.md new file mode 100644 index 0000000000000000000000000000000000000000..827da0a94eb33d73324a59af18a9736fd2d161f4 --- /dev/null +++ b/spaces/README.md @@ -0,0 +1,19 @@ +--- +title: GenAI DeepDetect +emoji: "🔍" +colorFrom: gray +colorTo: indigo +sdk: docker +app_port: 7860 +pinned: false +--- + +# GenAI-DeepDetect + +Docker-based Hugging Face Space for multimodal deepfake detection. + +This Space runs the FastAPI service from `src/api/main.py` and exposes: + +- `GET /health` +- `POST /detect/image` +- `POST /detect/video` diff --git a/spaces/app.py b/spaces/app.py new file mode 100644 index 0000000000000000000000000000000000000000..9e351922bcbc92b0559099a203490ff9f3d4df98 --- /dev/null +++ b/spaces/app.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +import os + +os.environ.setdefault("MODEL_CACHE_DIR", "/data/models") +os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") + +import uvicorn + +from src.api.main import app + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=7860, workers=1) diff --git a/spaces/examples/.gitkeep b/spaces/examples/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/spaces/requirements.txt b/spaces/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..e886535a7f96eff21a364d66edb794cda5f0606e --- /dev/null +++ b/spaces/requirements.txt @@ -0,0 +1,7 @@ +gradio>=4.36.1 +torch>=2.1.0 +torchvision>=0.16.0 +transformers>=4.40.0 +Pillow>=10.0.0 +numpy>=1.26.0 +httpx>=0.27.0 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/api/__init__.py b/src/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/api/demo_page.py b/src/api/demo_page.py new file mode 100644 index 0000000000000000000000000000000000000000..068b7def0984519135d406ea320f6a7f8e3e9e4b --- /dev/null +++ b/src/api/demo_page.py @@ -0,0 +1,48 @@ +"""src/api/demo_page.py — Minimal HTML demo page served at GET /""" + + +def render_demo() -> str: + return """ + + + + +GenAI-DeepDetect API + + + +
+
GenAI-DeepDetect · v1.0.0
+

Multimodal Deepfake Detection API

+

Three independent engines — Fingerprint (ViT), Coherence (lip-audio sync), SSTGNN (landmark graph) — fused into a single verdict.

+
    +
  • GET/healthStatus check
  • +
  • POST/detect/imageAnalyse image (max 20MB)
  • +
  • POST/detect/videoAnalyse video (max 500MB)
  • +
+ +
+ +""" diff --git a/src/api/main.py b/src/api/main.py new file mode 100644 index 0000000000000000000000000000000000000000..d976cd026a6e3544c2a9b89c7f0cba93f79fff86 --- /dev/null +++ b/src/api/main.py @@ -0,0 +1,438 @@ +from __future__ import annotations + +import asyncio +import io +import logging +import os +import sys +import tempfile +import time +from pathlib import Path + +import numpy as np +from dotenv import load_dotenv +from fastapi import FastAPI, File, HTTPException, UploadFile +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import HTMLResponse +from PIL import Image + +from src.engines.coherence.engine import CoherenceEngine +from src.engines.fingerprint.engine import FingerprintEngine +from src.engines.sstgnn.engine import SSTGNNEngine +from src.explainability.explainer import explain +from src.fusion.fuser import fuse +from src.services.hf_inference_client import HFInferenceClient, HFInferenceUnavailable +from src.services.inference_router import ( + get_inference_backend, + is_runpod_configured, + route_inference, +) +from src.types import DetectionResponse, EngineResult + +logger = logging.getLogger(__name__) + +# Load local development environment values from .env when present. +load_dotenv() + + +def _is_test_mode() -> bool: + return ( + os.environ.get("GENAI_SKIP_MODEL_LOAD", "").strip().lower() + in {"1", "true", "yes", "on"} + or "PYTEST_CURRENT_TEST" in os.environ + or "pytest" in sys.modules + ) + + +if _is_test_mode(): + os.environ.setdefault("GENAI_SKIP_MODEL_LOAD", "1") + + +app = FastAPI(title="GenAI-DeepDetect", version="1.0.0") +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +_fp = FingerprintEngine() +_co = CoherenceEngine() +_st = SSTGNNEngine() +_hf = HFInferenceClient() + +MAX_IMAGE_MB = int(os.environ.get("MAX_IMAGE_SIZE_MB", 20)) +MAX_VIDEO_MB = int(os.environ.get("MAX_VIDEO_SIZE_MB", 100)) +MAX_FRAMES = int(os.environ.get("MAX_VIDEO_FRAMES", 300)) + +IMAGE_TYPES = {"image/jpeg", "image/png", "image/webp", "image/bmp", "image/gif"} +VIDEO_TYPES = {"video/mp4", "video/quicktime", "video/x-msvideo", "video/webm", "video/avi"} + +SUPPORTED_GENERATORS = [ + "real", + "unknown_gan", + "stable_diffusion", + "midjourney", + "dall_e", + "flux", + "firefly", + "imagen", +] + + +def _model_inventory() -> dict[str, object]: + return { + "fingerprint": { + "primary_detector": "Organika/sdxl-detector", + "backup_detector": "haywoodsloan/ai-image-detector-deploy", + "attribution_model": "openai/clip-vit-large-patch14", + }, + "coherence": { + "hf_fallback_model": os.environ.get("COHERENCE_HF_MODEL_ID", "Wvolf/ViT_Deepfake_Detection"), + "facial_landmarks": "mediapipe FaceMesh/FaceLandmarker", + "temporal_embedding": "facenet-pytorch InceptionResnetV1(vggface2) when available", + }, + "sstgnn": { + "primary_detector": "dima806/deepfake_vs_real_image_detection", + "backup_detector": "prithivMLmods/Deep-Fake-Detector-Model", + "graph_component": "scipy.spatial.Delaunay + MediaPipe landmarks", + }, + "generator_labels": SUPPORTED_GENERATORS, + } + + +@app.get("/", response_class=HTMLResponse) +async def root() -> HTMLResponse: + return HTMLResponse("

GenAI-DeepDetect API

See /docs

") + + +@app.on_event("startup") +async def preload() -> None: + if _is_test_mode(): + logger.info("Skipping startup preload in test mode") + return + + logger.info("Preloading models...") + # Keep model imports/loads sequential to avoid lazy-import race issues. + await asyncio.to_thread(_fp._ensure) + await asyncio.to_thread(_co._ensure) + await asyncio.to_thread(_st._ensure) + logger.info("Model preload complete") + + +@app.get("/health") +async def health() -> dict: + return { + "status": "ok", + "version": "1.0.0", + "engines": ["fingerprint", "coherence", "sstgnn"], + "inference_backend": get_inference_backend(), + "runpod_configured": is_runpod_configured(), + } + + +@app.get("/health/models") +async def health_models() -> dict[str, object]: + """Return the pretrained model inventory used by each engine.""" + return _model_inventory() + + +def _extract_frames(path: str) -> list[np.ndarray]: + try: + import cv2 + except Exception as exc: + raise RuntimeError(f"OpenCV unavailable: {exc}") from exc + + cap = cv2.VideoCapture(path) + total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + step = max(1, total // MAX_FRAMES) if total > 0 else 1 + + frames: list[np.ndarray] = [] + index = 0 + while True: + ret, frame = cap.read() + if not ret: + break + if index % step == 0: + frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + index += 1 + if len(frames) >= MAX_FRAMES: + break + + cap.release() + return frames + + +def _assign_processing_time(results: list[EngineResult], ms: float) -> list[EngineResult]: + for result in results: + result.processing_time_ms = round(ms, 2) + return results + + +def _fallback_explanation(verdict: str, confidence: float, generator: str) -> str: + return ( + f"Content classified as {verdict} with {confidence:.0%} confidence. " + f"Attributed generator: {generator}." + ) + + +def _hf_fake_score(preds: list[dict]) -> float: + if not preds: + return 0.5 + + fake_keywords = ("fake", "deepfake", "generated", "synthetic", "artificial", "ai") + real_keywords = ("real", "authentic", "human") + + fake_best = 0.0 + real_best = 0.0 + for pred in preds: + label = str(pred.get("label", "")).strip().lower() + score = float(pred.get("score", 0.0)) + if any(keyword in label for keyword in fake_keywords): + fake_best = max(fake_best, score) + if any(keyword in label for keyword in real_keywords): + real_best = max(real_best, score) + + if fake_best == 0.0 and real_best == 0.0: + return 0.5 + if fake_best == 0.0: + return float(np.clip(1.0 - real_best, 0.0, 1.0)) + return float(np.clip(fake_best, 0.0, 1.0)) + + +def _hf_generator_label(preds: list[dict], verdict: str) -> str: + if verdict != "FAKE": + return "real" + labels = " ".join(str(pred.get("label", "")).lower() for pred in preds) + for candidate in SUPPORTED_GENERATORS: + if candidate == "real": + continue + if candidate.replace("_", " ") in labels or candidate in labels: + return candidate + return "unknown_gan" + + +def _build_hf_response(preds: list[dict], elapsed_ms: float, media_type: str) -> DetectionResponse: + fake_score = _hf_fake_score(preds) + verdict = "FAKE" if fake_score > 0.5 else "REAL" + confidence = fake_score if verdict == "FAKE" else (1.0 - fake_score) + generator = _hf_generator_label(preds, verdict) + + top_label = str(preds[0].get("label", "unknown")) if preds else "unknown" + explanation = ( + f"Hugging Face serverless ({media_type}) top label: {top_label}. " + f"Classified as {verdict} with {confidence:.0%} confidence." + ) + + engine_result = EngineResult( + engine="hf_serverless", + verdict=verdict, + confidence=confidence, + attributed_generator=generator, + explanation=explanation, + processing_time_ms=elapsed_ms, + ) + + return DetectionResponse( + verdict=verdict, + confidence=confidence, + attributed_generator=generator, + explanation=explanation, + processing_time_ms=elapsed_ms, + engine_breakdown=[engine_result], + ) + + +async def _hf_detect_image(data: bytes) -> DetectionResponse: + t0 = time.monotonic() + preds = await _hf.classify_image(data, timeout=45.0) + elapsed_ms = (time.monotonic() - t0) * 1000 + return _build_hf_response(preds, elapsed_ms, media_type="image") + + +async def _hf_detect_video(data: bytes) -> DetectionResponse: + with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp: + tmp.write(data) + tmp_path = tmp.name + + try: + frames = await asyncio.to_thread(_extract_frames, tmp_path) + finally: + Path(tmp_path).unlink(missing_ok=True) + + if not frames: + raise HTTPException(status_code=422, detail="Could not extract frames") + + keyframe = Image.fromarray(frames[0]) + buf = io.BytesIO() + keyframe.save(buf, format="JPEG") + return await _hf_detect_image(buf.getvalue()) + + +async def _ensure_models_loaded() -> None: + if _is_test_mode(): + return + await asyncio.to_thread(_fp._ensure) + await asyncio.to_thread(_co._ensure) + await asyncio.to_thread(_st._ensure) + + +@app.post("/detect/image", response_model=DetectionResponse) +async def detect_image(file: UploadFile = File(...)) -> DetectionResponse: + t0 = time.monotonic() + + content_type = (file.content_type or "").split(";")[0].strip().lower() + if content_type not in IMAGE_TYPES: + raise HTTPException(status_code=415, detail=f"Unsupported type: {file.content_type}") + + data = await file.read() + if len(data) > MAX_IMAGE_MB * 1024 * 1024: + raise HTTPException(status_code=413, detail="File too large") + + backend = get_inference_backend() + if backend == "hf" and not _is_test_mode(): + try: + return await _hf_detect_image(data) + except HFInferenceUnavailable as exc: + logger.warning("HF image route failed, trying RunPod fallback: %s", exc) + except Exception as exc: + logger.warning("HF image route unexpected error, trying RunPod fallback: %s", exc) + + if is_runpod_configured(): + try: + return await route_inference(data, "image") + except Exception as exc: + raise HTTPException( + status_code=503, + detail=f"Hugging Face and RunPod failed for image inference: {exc}", + ) from exc + + raise HTTPException( + status_code=503, + detail="Hugging Face inference failed and RunPod is not configured.", + ) + + if ( + backend == "runpod" + and not _is_test_mode() + and is_runpod_configured() + ): + try: + return await route_inference(data, "image") + except Exception as exc: + logger.warning("RunPod image route failed, falling back to local image inference: %s", exc) + + try: + image = Image.open(io.BytesIO(data)).convert("RGB") + except Exception as exc: + raise HTTPException(status_code=422, detail=f"Could not decode image: {exc}") from exc + + await _ensure_models_loaded() + + fp, co, st = await asyncio.gather( + asyncio.to_thread(_fp.run, image), + asyncio.to_thread(_co.run, image), + asyncio.to_thread(_st.run, image), + ) + + elapsed_ms = (time.monotonic() - t0) * 1000 + engine_results = _assign_processing_time([fp, co, st], elapsed_ms) + + verdict, conf, generator = fuse(engine_results, is_video=False) + if _is_test_mode(): + explanation = _fallback_explanation(verdict, conf, generator) + else: + explanation = await asyncio.to_thread(explain, verdict, conf, engine_results, generator) + + return DetectionResponse( + verdict=verdict, + confidence=conf, + attributed_generator=generator, + explanation=explanation, + processing_time_ms=elapsed_ms, + engine_breakdown=engine_results, + ) + + +@app.post("/detect/video", response_model=DetectionResponse) +async def detect_video(file: UploadFile = File(...)) -> DetectionResponse: + t0 = time.monotonic() + + content_type = (file.content_type or "").split(";")[0].strip().lower() + if content_type not in VIDEO_TYPES: + raise HTTPException(status_code=415, detail=f"Unsupported type: {file.content_type}") + + data = await file.read() + if len(data) > MAX_VIDEO_MB * 1024 * 1024: + raise HTTPException(status_code=413, detail="File too large") + + backend = get_inference_backend() + if backend == "hf" and not _is_test_mode(): + try: + return await _hf_detect_video(data) + except HFInferenceUnavailable as exc: + logger.warning("HF video route failed, trying RunPod fallback: %s", exc) + except Exception as exc: + logger.warning("HF video route unexpected error, trying RunPod fallback: %s", exc) + + if is_runpod_configured(): + try: + return await route_inference(data, "video") + except Exception as exc: + raise HTTPException( + status_code=503, + detail=f"Hugging Face and RunPod failed for video inference: {exc}", + ) from exc + + raise HTTPException( + status_code=503, + detail="Hugging Face inference failed and RunPod is not configured.", + ) + + should_try_runpod = ( + backend == "runpod" + or (backend == "auto" and len(data) > 20 * 1024 * 1024) + ) + if should_try_runpod and not _is_test_mode() and is_runpod_configured(): + try: + return await route_inference(data, "video") + except Exception as exc: + logger.warning("RunPod route failed, falling back to local video inference: %s", exc) + + with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp: + tmp.write(data) + tmp_path = tmp.name + + try: + frames = await asyncio.to_thread(_extract_frames, tmp_path) + finally: + Path(tmp_path).unlink(missing_ok=True) + + if not frames: + raise HTTPException(status_code=422, detail="Could not extract frames") + + await _ensure_models_loaded() + + fp, co, st = await asyncio.gather( + asyncio.to_thread(_fp.run_video, frames), + asyncio.to_thread(_co.run_video, frames), + asyncio.to_thread(_st.run_video, frames), + ) + + elapsed_ms = (time.monotonic() - t0) * 1000 + engine_results = _assign_processing_time([fp, co, st], elapsed_ms) + + verdict, conf, generator = fuse(engine_results, is_video=True) + if _is_test_mode(): + explanation = _fallback_explanation(verdict, conf, generator) + else: + explanation = await asyncio.to_thread(explain, verdict, conf, engine_results, generator) + + return DetectionResponse( + verdict=verdict, + confidence=conf, + attributed_generator=generator, + explanation=explanation, + processing_time_ms=elapsed_ms, + engine_breakdown=engine_results, + ) diff --git a/src/continual/__init__.py b/src/continual/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ff64c8883e514a39f1cdf5b8b6627695b121d449 --- /dev/null +++ b/src/continual/__init__.py @@ -0,0 +1 @@ +# src/continual — EWC (Elastic Weight Consolidation) continual learning diff --git a/src/continual/ewc.py b/src/continual/ewc.py new file mode 100644 index 0000000000000000000000000000000000000000..d4561bd208c2df486bc5515552d48bfc05d11328 --- /dev/null +++ b/src/continual/ewc.py @@ -0,0 +1,147 @@ +""" +src/continual/ewc.py — Elastic Weight Consolidation (EWC) + +Implements EWC penalty term to prevent catastrophic forgetting when +fine-tuning on new generator classes. + +EWC loss: + L_total = L_task_new + λ * Σ_i F_i * (θ_i - θ*_i)² + +where: + F_i = diagonal Fisher information for parameter i on the *old* task + θ*_i = parameter i's value immediately after old-task training + λ = importance weight (default 400, tunable in configs/) + +Reference: + Kirkpatrick et al. "Overcoming catastrophic forgetting in neural networks." + PNAS 2017. https://arxiv.org/abs/1612.00796 + +Usage: + ewc = EWC(model, dataloader_old_task) + loss = ewc.penalty(model) # add to task-specific loss before backward() +""" +from __future__ import annotations + +import copy +from typing import Iterator, Optional + +import torch +import torch.nn as nn +from torch import Tensor +from torch.utils.data import DataLoader + + +class EWC: + """ + Elastic Weight Consolidation penalty tracker. + + Parameters + ---------- + model: + PyTorch module whose important weights we want to preserve. + dataloader: + DataLoader over the *previous* task's training data (used to + estimate the diagonal Fisher information). + criterion: + Loss function used to compute per-sample gradients. + Defaults to CrossEntropyLoss. + num_samples: + Maximum number of samples used to estimate Fisher. Fewer samples + = faster, noisier estimate. + importance: + λ — EWC penalty weight. Higher → less forgetting, slower adaptation. + """ + + def __init__( + self, + model: nn.Module, + dataloader: DataLoader, + criterion: Optional[nn.Module] = None, + num_samples: int = 200, + importance: float = 400.0, + ) -> None: + self.model = model + self.importance = importance + self._criterion = criterion or nn.CrossEntropyLoss() + + # Snapshot θ* (old-task parameters) + self._params_star: dict[str, Tensor] = { + name: param.clone().detach() + for name, param in model.named_parameters() + if param.requires_grad + } + + # Estimate diagonal Fisher + self._fisher: dict[str, Tensor] = self._estimate_fisher( + model, dataloader, num_samples + ) + + # ------------------------------------------------------------------ + # Public + # ------------------------------------------------------------------ + + def penalty(self, model: nn.Module) -> Tensor: + """ + Compute the EWC penalty for the *current* model parameters. + + Returns a scalar tensor that can be added to the task loss. + """ + device = next(model.parameters()).device + loss = torch.tensor(0.0, device=device) + + for name, param in model.named_parameters(): + if not param.requires_grad: + continue + if name not in self._fisher: + continue + fisher = self._fisher[name].to(device) + star = self._params_star[name].to(device) + loss += (fisher * (param - star).pow(2)).sum() + + return self.importance * loss + + # ------------------------------------------------------------------ + # Private + # ------------------------------------------------------------------ + + def _estimate_fisher( + self, + model: nn.Module, + dataloader: DataLoader, + num_samples: int, + ) -> dict[str, Tensor]: + """Estimate diagonal Fisher via squared gradients on old-task data.""" + fisher: dict[str, Tensor] = { + name: torch.zeros_like(param) + for name, param in model.named_parameters() + if param.requires_grad + } + + model.eval() + n_seen = 0 + + for batch in dataloader: + if n_seen >= num_samples: + break + + images, labels = batch + device = next(model.parameters()).device + images, labels = images.to(device), labels.to(device) + + model.zero_grad() + output = model(images) + loss = self._criterion(output, labels) + loss.backward() + + for name, param in model.named_parameters(): + if param.requires_grad and param.grad is not None: + fisher[name] += param.grad.detach().pow(2) + + n_seen += images.size(0) + + # Normalize + if n_seen > 0: + for name in fisher: + fisher[name] /= n_seen + + return fisher diff --git a/src/continual/registry.py b/src/continual/registry.py new file mode 100644 index 0000000000000000000000000000000000000000..10a6085e6987371b1629201ed7f4ed6d9ccb2f03 --- /dev/null +++ b/src/continual/registry.py @@ -0,0 +1,121 @@ +""" +src/continual/registry.py — Continual-learning task registry + +Persists a JSON list of registered generator classes (tasks) to disk so +training scripts and the API can stay in sync. + +Schema (per entry): + { + "label": "sora", # generator label string + "sample_path": "data/sora/...", # representative sample path + "registered_at": "2025-01-01T00:00:00", + "weight_path": null # filled after EWC checkpoint is saved + } + +Usage: + registry = TaskRegistry() + registry.register("sora", "data/sora/frame_001.jpg") + tasks = registry.all_tasks() +""" +from __future__ import annotations + +import json +import logging +import os +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + +_DEFAULT_REGISTRY_PATH = Path("data/continual_registry.json") + + +class TaskRegistry: + """ + Simple JSON-backed task registry for continual-learning experiments. + + Parameters + ---------- + path: + Path to the JSON registry file. Created on first write. + """ + + def __init__(self, path: Path | str = _DEFAULT_REGISTRY_PATH) -> None: + self._path = Path(path) + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def register( + self, + label: str, + sample_path: str, + weight_path: str | None = None, + ) -> dict[str, Any]: + """ + Register a new generator-class task. + + Returns the new registry entry dict. + Raises ValueError if *label* already registered. + """ + tasks = self._load() + existing = [t for t in tasks if t["label"] == label] + if existing: + raise ValueError( + f"Task '{label}' already registered. " + f"Use update() to modify it." + ) + + entry: dict[str, Any] = { + "label": label, + "sample_path": sample_path, + "registered_at": datetime.now(timezone.utc).isoformat(), + "weight_path": weight_path, + } + tasks.append(entry) + self._save(tasks) + logger.info("Registered continual task: %s", label) + return entry + + def update(self, label: str, **fields: Any) -> dict[str, Any]: + """Update fields on an existing task entry.""" + tasks = self._load() + for task in tasks: + if task["label"] == label: + task.update(fields) + self._save(tasks) + return task + raise KeyError(f"Task '{label}' not found in registry.") + + def all_tasks(self) -> list[dict[str, Any]]: + """Return all registered tasks.""" + return self._load() + + def get(self, label: str) -> dict[str, Any] | None: + """Return a single task by label, or None.""" + for task in self._load(): + if task["label"] == label: + return task + return None + + # ------------------------------------------------------------------ + # Internal + # ------------------------------------------------------------------ + + def _load(self) -> list[dict[str, Any]]: + if not self._path.exists(): + return [] + try: + return json.loads(self._path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + logger.warning("Registry file corrupt; starting fresh.") + return [] + + def _save(self, tasks: list[dict[str, Any]]) -> None: + self._path.parent.mkdir(parents=True, exist_ok=True) + self._path.write_text( + json.dumps(tasks, indent=2, default=str), + encoding="utf-8", + ) diff --git a/src/engines/__init__.py b/src/engines/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..fd5f9e6daf7a241328212c571ae9dba1d98bd769 --- /dev/null +++ b/src/engines/__init__.py @@ -0,0 +1,12 @@ +from .fingerprint import FingerprintEngine, FingerprintDetector +from .coherence import CoherenceEngine, CoherenceDetector +from .sstgnn import SSTGNNEngine, SSTGNNDetector + +__all__ = [ + "FingerprintEngine", + "FingerprintDetector", + "CoherenceEngine", + "CoherenceDetector", + "SSTGNNEngine", + "SSTGNNDetector", +] diff --git a/src/engines/coherence/__init__.py b/src/engines/coherence/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6ccbe124e6137240e684a187f1d27d69d082c4bf --- /dev/null +++ b/src/engines/coherence/__init__.py @@ -0,0 +1,4 @@ +from .engine import CoherenceEngine +from .detector import CoherenceDetector + +__all__ = ["CoherenceEngine", "CoherenceDetector"] diff --git a/src/engines/coherence/detector.py b/src/engines/coherence/detector.py new file mode 100644 index 0000000000000000000000000000000000000000..87843c58827b57496629ecbad8adcfb421759190 --- /dev/null +++ b/src/engines/coherence/detector.py @@ -0,0 +1,75 @@ +"""Compatibility wrapper around `CoherenceEngine`. + +New code should import `src.engines.coherence.engine.CoherenceEngine` directly. +""" +from __future__ import annotations + +import os +import tempfile + +from src.types import EngineResult + +from .engine import CoherenceEngine + + +class CoherenceDetector(CoherenceEngine): + threshold = 0.5 + + def detect_bytes(self, video_bytes: bytes) -> EngineResult: + frames = self._extract_video_frames(video_bytes) + if not frames: + return self._error_result(0.0) + try: + return self.run_video(frames) + except Exception: + return self._error_result(0.0) + + def _extract_video_frames(self, video_bytes: bytes) -> list: + try: + import cv2 + except Exception: + return [] + + with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp: + tmp.write(video_bytes) + tmp_path = tmp.name + + frames = [] + try: + cap = cv2.VideoCapture(tmp_path) + index = 0 + while True: + ok, frame = cap.read() + if not ok: + break + if index % 2 == 0: + frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + index += 1 + if len(frames) >= 64: + break + cap.release() + return frames + finally: + os.unlink(tmp_path) + + @staticmethod + def image_stub() -> EngineResult: + return EngineResult( + engine="coherence", + verdict="REAL", + confidence=0.5, + attributed_generator=None, + explanation="N/A - coherence analysis requires video input.", + processing_time_ms=0.0, + ) + + @staticmethod + def _error_result(elapsed_s: float) -> EngineResult: + return EngineResult( + engine="coherence", + verdict="REAL", + confidence=0.5, + attributed_generator=None, + explanation="Coherence engine encountered an error - result is inconclusive.", + processing_time_ms=elapsed_s * 1000.0, + ) diff --git a/src/engines/coherence/engine.py b/src/engines/coherence/engine.py new file mode 100644 index 0000000000000000000000000000000000000000..c3473e79c82ea42128a1d66233865666af505893 --- /dev/null +++ b/src/engines/coherence/engine.py @@ -0,0 +1,387 @@ +from __future__ import annotations + +import logging +import os +import threading +import time +import urllib.request +from pathlib import Path +from typing import Any + +import numpy as np +from PIL import Image + +from src.types import EngineResult + +logger = logging.getLogger(__name__) + +_lock = threading.Lock() +_load_attempted = False +_mtcnn = None +_resnet = None +_face_mesh = None +_torch = None +_hf_detector = None + + +def _skip_model_loads() -> bool: + return os.environ.get("GENAI_SKIP_MODEL_LOAD", "").strip().lower() in { + "1", + "true", + "yes", + "on", + } + + +def _get_pipeline(): + try: + from transformers import pipeline as hf_pipeline # type: ignore + except Exception: + from transformers.pipelines import pipeline as hf_pipeline # type: ignore + return hf_pipeline + + +class _TasksFaceMeshAdapter: + def __init__(self, mp_module, landmarker) -> None: + self._mp = mp_module + self._landmarker = landmarker + + def process(self, frame: np.ndarray): + mp_image = self._mp.Image(image_format=self._mp.ImageFormat.SRGB, data=frame) + result = self._landmarker.detect(mp_image) + + class _Face: + def __init__(self, landmarks): + self.landmark = landmarks + + class _Result: + def __init__(self, faces): + self.multi_face_landmarks = faces + + faces = [_Face(landmarks) for landmarks in getattr(result, "face_landmarks", [])] + return _Result(faces) + + +def _ensure_face_landmarker_asset() -> Path: + cache_dir = Path(os.environ.get("MODEL_CACHE_DIR", "/tmp/models")) + model_path = cache_dir / "mediapipe" / "face_landmarker.task" + if model_path.exists(): + return model_path + + model_path.parent.mkdir(parents=True, exist_ok=True) + url = os.environ.get( + "MEDIAPIPE_FACE_LANDMARKER_URL", + ( + "https://storage.googleapis.com/mediapipe-models/face_landmarker/" + "face_landmarker/float16/latest/face_landmarker.task" + ), + ) + urllib.request.urlretrieve(url, model_path) + return model_path + + +def _build_face_mesh(): + import mediapipe as mp # type: ignore + + if hasattr(mp, "solutions"): + return mp.solutions.face_mesh.FaceMesh( + static_image_mode=False, + max_num_faces=1, + refine_landmarks=True, + min_detection_confidence=0.5, + ) + + from mediapipe.tasks import python as mp_tasks_python # type: ignore + from mediapipe.tasks.python import vision # type: ignore + + model_path = _ensure_face_landmarker_asset() + options = vision.FaceLandmarkerOptions( + base_options=mp_tasks_python.BaseOptions(model_asset_path=str(model_path)), + running_mode=vision.RunningMode.IMAGE, + num_faces=1, + ) + landmarker = vision.FaceLandmarker.create_from_options(options) + return _TasksFaceMeshAdapter(mp, landmarker) + + +def _build_image_classifier(model_id: str) -> Any: + pipeline = _get_pipeline() + + cache_dir = os.environ.get("MODEL_CACHE_DIR", "/tmp/models") + try: + return pipeline( + "image-classification", + model=model_id, + model_kwargs={"cache_dir": cache_dir}, + ) + except Exception: + return pipeline("image-classification", model=model_id) + + +def _load() -> None: + global _mtcnn, _resnet, _face_mesh, _load_attempted, _torch, _hf_detector + if _load_attempted: + return + + _load_attempted = True + if _skip_model_loads(): + logger.info("Skipping coherence model load (GENAI_SKIP_MODEL_LOAD=1)") + return + + logger.info("Loading coherence models...") + + try: + _face_mesh = _build_face_mesh() + except Exception as exc: + logger.warning("Coherence FaceMesh unavailable: %s", exc) + + try: + from facenet_pytorch import InceptionResnetV1, MTCNN # type: ignore + + _mtcnn = MTCNN(keep_all=False, device="cpu") + _resnet = InceptionResnetV1(pretrained="vggface2").eval() + + try: + import torch # type: ignore + + _torch = torch + except Exception: + _torch = None + + except Exception as exc: + logger.warning("Coherence embedding model load failed, using heuristic-only mode: %s", exc) + + try: + model_id = os.environ.get("COHERENCE_HF_MODEL_ID", "Wvolf/ViT_Deepfake_Detection") + _hf_detector = _build_image_classifier(model_id) + except Exception as exc: + logger.warning("Coherence HF fallback model unavailable: %s", exc) + + logger.info("Coherence model load attempt complete") + + +class CoherenceEngine: + def _ensure(self) -> None: + with _lock: + _load() + + def run(self, image: Image.Image) -> EngineResult: + t0 = time.perf_counter() + self._ensure() + + frame = np.array(image.convert("RGB")) + score = self._image_score(frame) + score = float(np.clip(score * 0.6 + self._hf_image_score(image) * 0.4, 0.0, 1.0)) + + return EngineResult( + engine="coherence", + verdict="FAKE" if score > 0.5 else "REAL", + confidence=float(np.clip(score, 0.0, 1.0)), + attributed_generator=None, + explanation=f"Geometric coherence anomaly {score:.2f} (image mode).", + processing_time_ms=(time.perf_counter() - t0) * 1000, + ) + + def _image_score(self, frame: np.ndarray) -> float: + if _face_mesh is None: + return 0.35 + + try: + res = _face_mesh.process(frame) + if not res.multi_face_landmarks: + return 0.35 + + lms = res.multi_face_landmarks[0].landmark + h, w = frame.shape[:2] + + def pt(index: int) -> np.ndarray: + return np.array([lms[index].x * w, lms[index].y * h], dtype=np.float32) + + lew = np.linalg.norm(pt(33) - pt(133)) + rew = np.linalg.norm(pt(362) - pt(263)) + eye_ratio = float(min(lew, rew) / (max(lew, rew) + 1e-9)) + eye_score = max(0.0, (0.85 - eye_ratio) / 0.3) + + nose = pt(1) + left_dist = np.linalg.norm(nose - pt(234)) + right_dist = np.linalg.norm(nose - pt(454)) + lr = min(left_dist, right_dist) + rr = max(left_dist, right_dist) + ear_score = max(0.0, (0.90 - lr / (rr + 1e-9)) / 0.2) + + return float(np.clip(eye_score * 0.5 + ear_score * 0.5, 0.0, 1.0)) + except Exception as exc: + logger.warning("Coherence image scoring failed: %s", exc) + return 0.35 + + def run_video(self, frames: list[np.ndarray]) -> EngineResult: + t0 = time.perf_counter() + self._ensure() + + if not frames: + return EngineResult( + engine="coherence", + verdict="UNKNOWN", + confidence=0.5, + attributed_generator=None, + explanation="No frames.", + processing_time_ms=0.0, + ) + + if len(frames) < 4: + result = self.run(Image.fromarray(frames[0])) + result.explanation = "Too few frames for temporal analysis." + return result + + delta = self._embedding_variance(frames) + jerk = self._landmark_jerk(frames) + blink = self._blink_anomaly(frames) + + hf_video = self._hf_video_score(frames) + score = float(np.clip(delta * 0.35 + jerk * 0.30 + blink * 0.15 + hf_video * 0.20, 0.0, 1.0)) + + return EngineResult( + engine="coherence", + verdict="FAKE" if score > 0.5 else "REAL", + confidence=score, + attributed_generator=None, + explanation=( + f"Embedding variance {delta:.2f}, " + f"landmark jerk {jerk:.2f}, " + f"blink anomaly {blink:.2f}, " + f"hf score {hf_video:.2f}." + ), + processing_time_ms=(time.perf_counter() - t0) * 1000, + ) + + def _hf_image_score(self, image: Image.Image) -> float: + if _hf_detector is None: + return 0.5 + try: + preds = _hf_detector(image) + return self._fake_score_from_preds(preds) + except Exception: + return 0.5 + + def _hf_video_score(self, frames: list[np.ndarray]) -> float: + if _hf_detector is None or not frames: + return 0.5 + values: list[float] = [] + for frame in frames[::8]: + try: + preds = _hf_detector(Image.fromarray(frame)) + values.append(self._fake_score_from_preds(preds)) + except Exception: + continue + if not values: + return 0.5 + return float(np.clip(np.mean(values), 0.0, 1.0)) + + def _fake_score_from_preds(self, preds: list[dict]) -> float: + if not preds: + return 0.5 + keywords = ("fake", "deepfake", "generated", "synthetic", "ai", "artificial") + best = 0.0 + for pred in preds: + label = str(pred.get("label", "")).lower() + score = float(pred.get("score", 0.0)) + if any(keyword in label for keyword in keywords): + best = max(best, score) + if best == 0.0: + return 0.5 + return float(np.clip(best, 0.0, 1.0)) + + def _embedding_variance(self, frames: list[np.ndarray]) -> float: + if _mtcnn is None or _resnet is None or _torch is None: + return 0.5 + + embeddings: list[np.ndarray] = [] + for frame in frames[::4]: + try: + face = _mtcnn(Image.fromarray(frame)) + if face is not None: + with _torch.no_grad(): + emb = _resnet(face.unsqueeze(0)).detach().cpu().numpy()[0] + embeddings.append(emb) + except Exception: + continue + + if len(embeddings) < 2: + return 0.5 + + deltas = [ + float(np.linalg.norm(embeddings[index + 1] - embeddings[index])) + for index in range(len(embeddings) - 1) + ] + return float(np.clip(np.var(deltas) * 8.0, 0.0, 1.0)) + + def _landmark_jerk(self, frames: list[np.ndarray]) -> float: + if _face_mesh is None: + return 0.3 + + positions: list[list[float]] = [] + for frame in frames[::2]: + try: + res = _face_mesh.process(frame) + if res.multi_face_landmarks: + lm = res.multi_face_landmarks[0].landmark + positions.append([lm[1].x, lm[1].y]) + except Exception: + continue + + if len(positions) < 4: + return 0.3 + + pos = np.array(positions, dtype=np.float32) + jerk = np.diff(pos, n=3, axis=0) + jerk_norm = np.linalg.norm(jerk, axis=1) + return float(np.clip((float(np.mean(jerk_norm)) - 0.002) / 0.008, 0.0, 1.0)) + + def _blink_anomaly(self, frames: list[np.ndarray]) -> float: + if _face_mesh is None: + return 0.3 + + left_eye = [33, 160, 158, 133, 153, 144] + right_eye = [362, 385, 387, 263, 373, 380] + + def ear(lms, idx, h: int, w: int) -> float: + pts = [np.array([lms[i].x * w, lms[i].y * h], dtype=np.float32) for i in idx] + a = np.linalg.norm(pts[1] - pts[5]) + b = np.linalg.norm(pts[2] - pts[4]) + c = np.linalg.norm(pts[0] - pts[3]) + return float((a + b) / (2.0 * c + 1e-9)) + + ears: list[float] = [] + for frame in frames: + try: + res = _face_mesh.process(frame) + if res.multi_face_landmarks: + lm = res.multi_face_landmarks[0].landmark + h, w = frame.shape[:2] + ears.append((ear(lm, left_eye, h, w) + ear(lm, right_eye, h, w)) / 2.0) + except Exception: + continue + + if len(ears) < 10: + return 0.3 + + arr = np.array(ears, dtype=np.float32) + blinks = int(np.sum(np.diff((arr < 0.21).astype(int)) > 0)) + bpm = blinks / (len(ears) / 25.0) * 60.0 + + if 8.0 <= bpm <= 25.0: + return 0.15 + if bpm < 3.0 or bpm > 35.0: + return 0.80 + return 0.45 + + @staticmethod + def image_stub() -> EngineResult: + return EngineResult( + engine="coherence", + verdict="UNKNOWN", + confidence=0.5, + attributed_generator=None, + explanation="N/A - coherence analysis requires video input.", + processing_time_ms=0.0, + ) diff --git a/src/engines/fingerprint/__init__.py b/src/engines/fingerprint/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bd174f536d30e1ab754897dfed77b71f8f08e632 --- /dev/null +++ b/src/engines/fingerprint/__init__.py @@ -0,0 +1,4 @@ +from .engine import FingerprintEngine +from .detector import FingerprintDetector + +__all__ = ["FingerprintEngine", "FingerprintDetector"] diff --git a/src/engines/fingerprint/detector.py b/src/engines/fingerprint/detector.py new file mode 100644 index 0000000000000000000000000000000000000000..85f6b2f5feacb0837f5ac165dcfb4bef3f03d711 --- /dev/null +++ b/src/engines/fingerprint/detector.py @@ -0,0 +1,85 @@ +"""Compatibility wrapper around `FingerprintEngine`. + +New code should import `src.engines.fingerprint.engine.FingerprintEngine` directly. +""" +from __future__ import annotations + +import io +import os +import tempfile + +from PIL import Image + +from src.types import EngineResult + +from .engine import FingerprintEngine + + +class FingerprintDetector(FingerprintEngine): + """Backwards-compatible detector API.""" + + threshold = 0.5 + model = object() + + def detect_bytes(self, raw_bytes: bytes, is_video: bool = False) -> EngineResult: + try: + if is_video: + frames = self._extract_video_frames(raw_bytes) + if not frames: + return self._error_result("No frames extracted from video.") + return self.run_video(frames) + + image = Image.open(io.BytesIO(raw_bytes)).convert("RGB") + return self.run(image) + except Exception as exc: + return self._error_result(str(exc)) + + def _extract_video_frames(self, raw_bytes: bytes) -> list: + try: + import cv2 + except Exception: + return [] + + with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp: + tmp.write(raw_bytes) + tmp_path = tmp.name + + frames = [] + try: + cap = cv2.VideoCapture(tmp_path) + index = 0 + while True: + ok, frame = cap.read() + if not ok: + break + if index % 8 == 0: + frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + index += 1 + if len(frames) >= 32: + break + cap.release() + return frames + finally: + os.unlink(tmp_path) + + @staticmethod + def image_stub() -> EngineResult: + return EngineResult( + engine="fingerprint", + verdict="REAL", + confidence=0.5, + attributed_generator=None, + explanation="N/A", + processing_time_ms=0.0, + ) + + @staticmethod + def _error_result(reason: str) -> EngineResult: + return EngineResult( + engine="fingerprint", + verdict="REAL", + confidence=0.5, + attributed_generator=None, + explanation=f"Fingerprint engine error: {reason}", + processing_time_ms=0.0, + ) diff --git a/src/engines/fingerprint/engine.py b/src/engines/fingerprint/engine.py new file mode 100644 index 0000000000000000000000000000000000000000..925827ebd1a4668f15f4a1899d3bb74aa9fef4b5 --- /dev/null +++ b/src/engines/fingerprint/engine.py @@ -0,0 +1,230 @@ +from __future__ import annotations + +import io +import logging +import os +import threading +import time +from collections import Counter +from typing import Any + +import numpy as np +from PIL import Image + +from src.types import EngineResult + +logger = logging.getLogger(__name__) +CACHE = os.environ.get("MODEL_CACHE_DIR", "/tmp/models") + +GENERATOR_PROMPTS = { + "real": "a real photograph taken by a camera with natural lighting", + "unknown_gan": "a GAN-generated image with checkerboard artifacts and blurry edges", + "stable_diffusion": "a Stable Diffusion image with painterly soft textures", + "midjourney": "a Midjourney image with cinematic dramatic lighting and hyperdetail", + "dall_e": "a DALL-E image with clean illustration-style and smooth gradients", + "flux": "a FLUX model image with photorealistic precision and sharp detail", + "firefly": "an Adobe Firefly image with commercial stock-photo aesthetics", + "imagen": "a Google Imagen image with precise photorealistic rendering", +} + +_FAKE_KEYWORDS = ("artificial", "fake", "ai", "generated", "deepfake", "synthetic") + +_lock = threading.Lock() +_load_attempted = False +_detector = None +_clip_zeroshot = None +_backup = None + + +def _skip_model_loads() -> bool: + return os.environ.get("GENAI_SKIP_MODEL_LOAD", "").strip().lower() in { + "1", + "true", + "yes", + "on", + } + + +def _get_pipeline(): + try: + from transformers import pipeline as hf_pipeline # type: ignore + except Exception: + from transformers.pipelines import pipeline as hf_pipeline # type: ignore + return hf_pipeline + + +def _build_image_classifier(model_id: str) -> Any: + pipeline = _get_pipeline() + + # transformers>=5 changed some pipeline kwargs behavior; keep both paths. + try: + return pipeline( + "image-classification", + model=model_id, + model_kwargs={"cache_dir": CACHE}, + ) + except Exception: + return pipeline("image-classification", model=model_id) + + +def _build_zero_shot_image_classifier(model_id: str) -> Any: + pipeline = _get_pipeline() + + try: + return pipeline( + "zero-shot-image-classification", + model=model_id, + model_kwargs={"cache_dir": CACHE}, + ) + except Exception: + return pipeline("zero-shot-image-classification", model=model_id) + + +def _load() -> None: + global _detector, _clip_zeroshot, _backup, _load_attempted + if _load_attempted: + return + + _load_attempted = True + if _skip_model_loads(): + logger.info("Skipping fingerprint model load (GENAI_SKIP_MODEL_LOAD=1)") + return + + logger.info("Loading fingerprint models...") + + try: + _detector = _build_image_classifier("Organika/sdxl-detector") + _clip_zeroshot = _build_zero_shot_image_classifier("openai/clip-vit-large-patch14") + + try: + _backup = _build_image_classifier("haywoodsloan/ai-image-detector-deploy") + except Exception: + logger.warning("Backup fingerprint detector unavailable") + + except Exception as exc: + logger.warning("Fingerprint models unavailable: %s", exc) + + logger.info("Fingerprint model load attempt complete") + + +def _fake_score(preds: list[dict]) -> float: + if not preds: + return 0.5 + + best = 0.0 + for pred in preds: + label = str(pred.get("label", "")).lower() + score = float(pred.get("score", 0.0)) + if any(keyword in label for keyword in _FAKE_KEYWORDS): + best = max(best, score) + + if best == 0.0: + return 0.5 + return float(np.clip(best, 0.0, 1.0)) + + +class FingerprintEngine: + def _ensure(self) -> None: + with _lock: + _load() + + def run(self, image: Image.Image) -> EngineResult: + t0 = time.perf_counter() + self._ensure() + + if image.mode != "RGB": + image = image.convert("RGB") + + fake_score = 0.5 + try: + if _detector is not None: + fake_score = _fake_score(_detector(image)) + except Exception as exc: + logger.warning("Primary detector error: %s", exc) + + if _backup is not None: + try: + backup_score = _fake_score(_backup(image)) + fake_score = float(np.clip(fake_score * 0.6 + backup_score * 0.4, 0.0, 1.0)) + except Exception: + pass + + generator = "real" + try: + if _clip_zeroshot is not None: + labels = list(GENERATOR_PROMPTS.values()) + zs = _clip_zeroshot(image, candidate_labels=labels) + if zs: + top_label = str(zs[0].get("label", "")) + for key, prompt in GENERATOR_PROMPTS.items(): + if prompt == top_label: + generator = key + break + except Exception as exc: + logger.warning("CLIP attribution error: %s", exc) + + if fake_score > 0.65 and generator == "real": + generator = "unknown_gan" + + elapsed_ms = (time.perf_counter() - t0) * 1000 + return EngineResult( + engine="fingerprint", + verdict="FAKE" if fake_score > 0.5 else "REAL", + confidence=float(np.clip(fake_score, 0.0, 1.0)), + attributed_generator=generator, + explanation=f"Binary score {fake_score:.2f}; attributed to {generator}.", + processing_time_ms=elapsed_ms, + ) + + def run_video(self, frames: list[np.ndarray]) -> EngineResult: + t0 = time.perf_counter() + if not frames: + return EngineResult( + engine="fingerprint", + verdict="UNKNOWN", + confidence=0.5, + attributed_generator="unknown_gan", + explanation="No frames.", + processing_time_ms=0.0, + ) + + keyframes = frames[::8] or [frames[0]] + results = [self.run(Image.fromarray(frame)) for frame in keyframes] + + avg = float(np.mean([r.confidence for r in results])) + generators = [ + r.attributed_generator + for r in results + if r.attributed_generator and r.attributed_generator != "real" + ] + top_gen = Counter(generators).most_common(1)[0][0] if generators else "real" + + verdict = "FAKE" if avg > 0.5 else "REAL" + if verdict == "REAL": + top_gen = "real" + elif top_gen == "real": + top_gen = "unknown_gan" + + return EngineResult( + engine="fingerprint", + verdict=verdict, + confidence=avg, + attributed_generator=top_gen, + explanation=f"Keyframe average {avg:.2f} over {len(keyframes)} frames.", + processing_time_ms=(time.perf_counter() - t0) * 1000, + ) + + @staticmethod + def image_stub() -> EngineResult: + return EngineResult( + engine="fingerprint", + verdict="UNKNOWN", + confidence=0.5, + attributed_generator=None, + explanation="N/A", + processing_time_ms=0.0, + ) + + def run_bytes(self, data: bytes) -> EngineResult: + image = Image.open(io.BytesIO(data)).convert("RGB") + return self.run(image) diff --git a/src/engines/sstgnn/__init__.py b/src/engines/sstgnn/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3f6a250cd3c75c503d2789d2b809392894b9196f --- /dev/null +++ b/src/engines/sstgnn/__init__.py @@ -0,0 +1,4 @@ +from .engine import SSTGNNEngine +from .detector import SSTGNNDetector + +__all__ = ["SSTGNNEngine", "SSTGNNDetector"] diff --git a/src/engines/sstgnn/detector.py b/src/engines/sstgnn/detector.py new file mode 100644 index 0000000000000000000000000000000000000000..d07274f24d62b39105108b7453a34885daf58627 --- /dev/null +++ b/src/engines/sstgnn/detector.py @@ -0,0 +1,94 @@ +"""Compatibility wrapper around `SSTGNNEngine`. + +New code should import `src.engines.sstgnn.engine.SSTGNNEngine` directly. +""" +from __future__ import annotations + +import os +import tempfile +import time + +import numpy as np + +from src.types import EngineResult + +from .engine import SSTGNNEngine + + +class SSTGNNDetector(SSTGNNEngine): + threshold = 0.5 + + def detect_bytes(self, video_bytes: bytes) -> EngineResult: + frames = self._extract_video_frames(video_bytes) + if not frames: + return self._error_result(0.0, "No frames") + try: + return self.run_video(frames) + except Exception as exc: + return self._error_result(0.0, str(exc)) + + def _extract_video_frames(self, video_bytes: bytes) -> list: + try: + import cv2 + except Exception: + return [] + + with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp: + tmp.write(video_bytes) + tmp_path = tmp.name + + frames = [] + try: + cap = cv2.VideoCapture(tmp_path) + index = 0 + while True: + ok, frame = cap.read() + if not ok: + break + if index % 4 == 0: + frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + index += 1 + if len(frames) >= 96: + break + cap.release() + return frames + finally: + os.unlink(tmp_path) + + def _detect_heuristic(self, landmark_seq: np.ndarray, t0: float) -> EngineResult: + velocities = np.diff(landmark_seq[:, :, :2], axis=0) + vel_std = float(np.std(velocities)) + anomaly = float(np.clip(1.0 - min(vel_std * 10.0, 1.0), 0.0, 1.0)) + is_fake = anomaly > self.threshold + confidence = anomaly if is_fake else (1.0 - anomaly) + + return EngineResult( + engine="sstgnn", + verdict="FAKE" if is_fake else "REAL", + confidence=confidence, + attributed_generator=None, + explanation=f"Heuristic landmark variance {vel_std:.4f}.", + processing_time_ms=max(0.0, (time.perf_counter() - t0) * 1000), + ) + + @staticmethod + def image_stub() -> EngineResult: + return EngineResult( + engine="sstgnn", + verdict="REAL", + confidence=0.5, + attributed_generator=None, + explanation="N/A - SSTGNN requires video input.", + processing_time_ms=0.0, + ) + + @staticmethod + def _error_result(elapsed_s: float, reason: str = "") -> EngineResult: + return EngineResult( + engine="sstgnn", + verdict="REAL", + confidence=0.5, + attributed_generator=None, + explanation=f"SSTGNN engine unavailable ({reason}) - result is inconclusive.", + processing_time_ms=elapsed_s * 1000.0, + ) diff --git a/src/engines/sstgnn/engine.py b/src/engines/sstgnn/engine.py new file mode 100644 index 0000000000000000000000000000000000000000..54c87b17ac6999201d762b651f4e9b3ec3b0373c --- /dev/null +++ b/src/engines/sstgnn/engine.py @@ -0,0 +1,293 @@ +from __future__ import annotations + +import logging +import os +import threading +import time +import urllib.request +from pathlib import Path +from typing import Any + +import numpy as np +from PIL import Image + +from src.types import EngineResult + +logger = logging.getLogger(__name__) +CACHE = os.environ.get("MODEL_CACHE_DIR", "/tmp/models") + +_lock = threading.Lock() +_load_attempted = False +_det1 = None +_det2 = None +_mesh = None +_delaunay = None + + +def _skip_model_loads() -> bool: + return os.environ.get("GENAI_SKIP_MODEL_LOAD", "").strip().lower() in { + "1", + "true", + "yes", + "on", + } + + +def _get_pipeline(): + try: + from transformers import pipeline as hf_pipeline # type: ignore + except Exception: + from transformers.pipelines import pipeline as hf_pipeline # type: ignore + return hf_pipeline + + +KEYPOINT_STEP = 7 +KEYPOINT_COUNT = 68 + + +class _TasksFaceMeshAdapter: + def __init__(self, mp_module, landmarker) -> None: + self._mp = mp_module + self._landmarker = landmarker + + def process(self, frame: np.ndarray): + mp_image = self._mp.Image(image_format=self._mp.ImageFormat.SRGB, data=frame) + result = self._landmarker.detect(mp_image) + + class _Face: + def __init__(self, landmarks): + self.landmark = landmarks + + class _Result: + def __init__(self, faces): + self.multi_face_landmarks = faces + + faces = [_Face(landmarks) for landmarks in getattr(result, "face_landmarks", [])] + return _Result(faces) + + +def _ensure_face_landmarker_asset() -> Path: + cache_dir = Path(os.environ.get("MODEL_CACHE_DIR", "/tmp/models")) + model_path = cache_dir / "mediapipe" / "face_landmarker.task" + if model_path.exists(): + return model_path + + model_path.parent.mkdir(parents=True, exist_ok=True) + url = os.environ.get( + "MEDIAPIPE_FACE_LANDMARKER_URL", + ( + "https://storage.googleapis.com/mediapipe-models/face_landmarker/" + "face_landmarker/float16/latest/face_landmarker.task" + ), + ) + urllib.request.urlretrieve(url, model_path) + return model_path + + +def _build_image_classifier(model_id: str) -> Any: + pipeline = _get_pipeline() + + try: + return pipeline( + "image-classification", + model=model_id, + model_kwargs={"cache_dir": CACHE}, + ) + except Exception: + return pipeline("image-classification", model=model_id) + + +def _build_face_mesh() -> Any: + import mediapipe as mp # type: ignore + + if hasattr(mp, "solutions"): + return mp.solutions.face_mesh.FaceMesh( + static_image_mode=True, + max_num_faces=1, + refine_landmarks=True, + ) + + from mediapipe.tasks import python as mp_tasks_python # type: ignore + from mediapipe.tasks.python import vision # type: ignore + + model_path = _ensure_face_landmarker_asset() + options = vision.FaceLandmarkerOptions( + base_options=mp_tasks_python.BaseOptions(model_asset_path=str(model_path)), + running_mode=vision.RunningMode.IMAGE, + num_faces=1, + ) + landmarker = vision.FaceLandmarker.create_from_options(options) + return _TasksFaceMeshAdapter(mp, landmarker) + + +def _load() -> None: + global _det1, _det2, _mesh, _delaunay, _load_attempted + if _load_attempted: + return + + _load_attempted = True + if _skip_model_loads(): + logger.info("Skipping SSTGNN model load (GENAI_SKIP_MODEL_LOAD=1)") + return + + logger.info("Loading SSTGNN models...") + + try: + _det1 = _build_image_classifier("dima806/deepfake_vs_real_image_detection") + try: + _det2 = _build_image_classifier("prithivMLmods/Deep-Fake-Detector-Model") + except Exception: + logger.warning("SSTGNN backup detector unavailable") + except Exception as exc: + logger.warning("SSTGNN HF detector load failed: %s", exc) + + try: + _mesh = _build_face_mesh() + except Exception as exc: + logger.warning("MediaPipe FaceMesh unavailable for SSTGNN: %s", exc) + + try: + from scipy.spatial import Delaunay # type: ignore + + _delaunay = Delaunay + except Exception: + _delaunay = None + + logger.info("SSTGNN model load attempt complete") + + +def _fake_prob(preds: list[dict]) -> float: + fake_keywords = ("fake", "deepfake", "artificial", "generated", "ai", "synthetic") + best = 0.0 + for pred in preds: + label = str(pred.get("label", "")).lower() + score = float(pred.get("score", 0.0)) + if any(keyword in label for keyword in fake_keywords): + best = max(best, score) + if best == 0.0: + return 0.5 + return float(np.clip(best, 0.0, 1.0)) + + +class SSTGNNEngine: + def _ensure(self) -> None: + with _lock: + _load() + + def run(self, image: Image.Image) -> EngineResult: + t0 = time.perf_counter() + self._ensure() + + if image.mode != "RGB": + image = image.convert("RGB") + + scores: list[float] = [] + try: + if _det1 is not None: + scores.append(_fake_prob(_det1(image)) * 0.6) + except Exception as exc: + logger.warning("SSTGNN det1 error: %s", exc) + + if _det2 is not None: + try: + scores.append(_fake_prob(_det2(image)) * 0.4) + except Exception as exc: + logger.warning("SSTGNN det2 error: %s", exc) + + if not scores: + return EngineResult( + engine="sstgnn", + verdict="REAL", + confidence=0.5, + attributed_generator=None, + explanation="All detectors failed; returning neutral score.", + processing_time_ms=(time.perf_counter() - t0) * 1000, + ) + + cnn = sum(scores) / (0.6 if len(scores) == 1 else 1.0) + graph = self._geometry_score(np.array(image)) + final = float(np.clip(cnn * 0.7 + graph * 0.3, 0.0, 1.0)) + + return EngineResult( + engine="sstgnn", + verdict="FAKE" if final > 0.5 else "REAL", + confidence=final, + attributed_generator=None, + explanation=f"CNN {cnn:.2f}, geometric graph anomaly {graph:.2f}.", + processing_time_ms=(time.perf_counter() - t0) * 1000, + ) + + def _geometry_score(self, frame: np.ndarray) -> float: + if _mesh is None: + return 0.3 + + try: + res = _mesh.process(frame) + if not res.multi_face_landmarks: + return 0.3 + + h, w = frame.shape[:2] + landmarks = res.multi_face_landmarks[0].landmark + idxs = list(range(0, 468, KEYPOINT_STEP))[:KEYPOINT_COUNT] + pts = np.array( + [[landmarks[i].x * w, landmarks[i].y * h] for i in idxs], + dtype=np.float32, + ) + + if _delaunay is None: + return 0.3 + + tri = _delaunay(pts) + areas = [] + for simplex in tri.simplices: + a, b, c = pts[simplex] + areas.append(abs(np.cross(b - a, c - a)) / 2.0) + + if not areas: + return 0.3 + + arr = np.array(areas, dtype=np.float32) + cv_score = float(np.std(arr) / (np.mean(arr) + 1e-9)) + return float(np.clip((cv_score - 0.8) / 1.5, 0.0, 1.0)) + + except Exception as exc: + logger.warning("Geometry score error: %s", exc) + return 0.3 + + def run_video(self, frames: list[np.ndarray]) -> EngineResult: + t0 = time.perf_counter() + self._ensure() + + if not frames: + return EngineResult( + engine="sstgnn", + verdict="REAL", + confidence=0.5, + attributed_generator=None, + explanation="No frames.", + processing_time_ms=0.0, + ) + + sample = frames[::6] or [frames[0]] + results = [self.run(Image.fromarray(frame)) for frame in sample] + avg = float(np.mean([r.confidence for r in results])) + + return EngineResult( + engine="sstgnn", + verdict="FAKE" if avg > 0.5 else "REAL", + confidence=avg, + attributed_generator=None, + explanation=f"Frame-sampled SSTGNN average {avg:.2f} over {len(sample)} frames.", + processing_time_ms=(time.perf_counter() - t0) * 1000, + ) + + @staticmethod + def image_stub() -> EngineResult: + return EngineResult( + engine="sstgnn", + verdict="UNKNOWN", + confidence=0.5, + attributed_generator=None, + explanation="N/A - SSTGNN requires video input.", + processing_time_ms=0.0, + ) diff --git a/src/engines/sstgnn/graph_builder.py b/src/engines/sstgnn/graph_builder.py new file mode 100644 index 0000000000000000000000000000000000000000..0355b02d560c70424ef5698c577b144a75a3534f --- /dev/null +++ b/src/engines/sstgnn/graph_builder.py @@ -0,0 +1,106 @@ +""" +src/engines/sstgnn/graph_builder.py + +Graph construction for SSTGNN inference. +This is a direct copy of training/phase3_sstgnn/build_graph.py. +Keep both files in sync if the graph spec changes. +""" +from __future__ import annotations + +import math + +import numpy as np +import torch + +try: + from torch_geometric.data import Data + _TORCH_GEOMETRIC_AVAILABLE = True +except ImportError: + _TORCH_GEOMETRIC_AVAILABLE = False + + +def _build_adjacency_list(): + edges = [] + edges += [(i, i + 1) for i in range(16)] # jaw + edges += [(i, i + 1) for i in range(17, 21)] # right eyebrow + edges += [(i, i + 1) for i in range(22, 26)] # left eyebrow + edges += [(i, i + 1) for i in range(27, 30)] # nose bridge + edges += [(i, i + 1) for i in range(31, 35)] # nose base + edges += [(30, 35)] + edges += [(36,37),(37,38),(38,39),(39,40),(40,41),(41,36)] # right eye + edges += [(42,43),(43,44),(44,45),(45,46),(46,47),(47,42)] # left eye + edges += [(i, i+1) for i in range(48, 59)] + [(59, 48)] # mouth outer + edges += [(i, i+1) for i in range(60, 67)] + [(67, 60)] # mouth inner + return edges + + +SPATIAL_EDGES = _build_adjacency_list() +TEMPORAL_WINDOW = 3 + + +def build_temporal_graph(landmark_sequence: np.ndarray, label: int = -1): + """ + Convert (T, L, 3) landmark sequence to torch_geometric Data. + + Args: + landmark_sequence: (T, L, 3) float array, xyz in [0,1] + label: binary label (-1 for inference) + + Returns: + torch_geometric.data.Data + """ + if not _TORCH_GEOMETRIC_AVAILABLE: + raise ImportError( + "torch_geometric is required for SSTGNN inference. " + "Install per KAGGLE.md Cell 1." + ) + + T, L, _ = landmark_sequence.shape + + # Node features: (x, y, z, frame_t/T, landmark_idx/L) + node_feats = [] + for t in range(T): + for l in range(L): + x, y, z = landmark_sequence[t, l] + node_feats.append([ + float(x), float(y), float(z), + t / max(T - 1, 1), + l / max(L - 1, 1), + ]) + + node_tensor = torch.tensor(node_feats, dtype=torch.float32) + + src_nodes, dst_nodes, weights = [], [], [] + + def nid(t, l): + return t * L + l + + # Spatial edges + for t in range(T): + for i, j in SPATIAL_EDGES: + if i < L and j < L: + u, v = nid(t, i), nid(t, j) + src_nodes += [u, v]; dst_nodes += [v, u]; weights += [1.0, 1.0] + + # Temporal edges + for t in range(T): + for dt in range(1, TEMPORAL_WINDOW + 1): + t2 = t + dt + if t2 >= T: + break + w = math.exp(-dt) + for l in range(L): + u, v = nid(t, l), nid(t2, l) + src_nodes += [u, v]; dst_nodes += [v, u]; weights += [w, w] + + # Self edges + for n in range(T * L): + src_nodes.append(n); dst_nodes.append(n); weights.append(1.0) + + edge_index = torch.tensor([src_nodes, dst_nodes], dtype=torch.long) + edge_attr = torch.tensor(weights, dtype=torch.float32).unsqueeze(1) + + data = Data(x=node_tensor, edge_index=edge_index, edge_attr=edge_attr) + if label >= 0: + data.y = torch.tensor([label], dtype=torch.float32) + return data diff --git a/src/explainability/__init__.py b/src/explainability/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/explainability/explainer.py b/src/explainability/explainer.py new file mode 100644 index 0000000000000000000000000000000000000000..1ed51eadb10d7288a242bd05b3bf1bb828b11b05 --- /dev/null +++ b/src/explainability/explainer.py @@ -0,0 +1,288 @@ +from __future__ import annotations + +import logging +import os +import queue +import threading + +from src.types import DetectionResponse, EngineResult + +logger = logging.getLogger(__name__) + +try: + from google import genai as genai_new # type: ignore +except Exception: + genai_new = None + +genai_legacy = None + + +SYSTEM_INSTRUCTION = ( + "You are a deepfake forensics analyst writing reports for security professionals. " + "Given detection engine outputs, write exactly 2-3 sentences in plain English " + "explaining why the content is real or fake. " + "Be specific and name the strongest signals. " + "Use direct declarative sentences. " + "Output only the explanation text." +) + +MODEL_CANDIDATES = ( + "gemini-2.5-pro", + "gemini-2.5-pro-latest", + "gemini-2.0-flash", + "gemini-1.5-pro", + "gemini-1.5-pro-latest", + # legacy names kept as last-resort candidates + "gemini-2.5-pro-preview-03-25", + "gemini-1.5-pro-002", +) + +REQUEST_TIMEOUT_S = float(os.environ.get("GEMINI_REQUEST_TIMEOUT_S", "10")) +MAX_MODEL_ATTEMPTS = max(1, int(os.environ.get("GEMINI_MAX_MODEL_ATTEMPTS", "3"))) +ENABLE_LEGACY_MODEL_DISCOVERY = os.environ.get("GEMINI_DISCOVER_MODELS", "").strip().lower() in { + "1", + "true", + "yes", + "on", +} + +_new_client = None +_legacy_model = None +_legacy_model_name = None +_legacy_candidates = None + + +def _get_api_key() -> str: + return os.environ.get("GEMINI_API_KEY", "").strip() + + +def _run_with_timeout(func, timeout_s: float): + result_q: queue.Queue[tuple[bool, object]] = queue.Queue(maxsize=1) + + def _runner() -> None: + try: + result_q.put((True, func())) + except Exception as exc: # pragma: no cover - passthrough + result_q.put((False, exc)) + + thread = threading.Thread(target=_runner, daemon=True) + thread.start() + + try: + ok, payload = result_q.get(timeout=timeout_s) + except queue.Empty as exc: + raise TimeoutError(f"Gemini request timed out after {timeout_s:.1f}s") from exc + + if ok: + return payload + raise payload # type: ignore[misc] + + +def _ensure_new_client(): + global _new_client + if _new_client is not None: + return _new_client + if genai_new is None: + return None + + api_key = _get_api_key() + if not api_key: + return None + + try: + _new_client = genai_new.Client(api_key=api_key) + return _new_client + except Exception as exc: + logger.warning("Failed to init google.genai client: %s", exc) + return None + + +def _generate_with_new_sdk(prompt: str) -> str: + client = _ensure_new_client() + if client is None: + raise RuntimeError("google.genai client unavailable") + + full_prompt = f"{SYSTEM_INSTRUCTION}\n\n{prompt}" + last_error: Exception | None = None + + for model_name in MODEL_CANDIDATES: + try: + response = _run_with_timeout( + lambda: client.models.generate_content( + model=model_name, + contents=full_prompt, + ), + REQUEST_TIMEOUT_S, + ) + text = getattr(response, "text", None) + if text and str(text).strip(): + logger.info("Gemini explain model selected (new SDK): %s", model_name) + return str(text).strip() + except Exception as exc: + last_error = exc + logger.debug("Gemini model %s failed on new SDK: %s", model_name, exc) + + if last_error: + raise last_error + raise RuntimeError("No Gemini model succeeded via new SDK") + + +def _ensure_legacy_configured() -> bool: + global genai_legacy + if genai_legacy is None: + try: + import google.generativeai as _legacy # type: ignore + genai_legacy = _legacy + except Exception: + return False + + if genai_legacy is None: + return False + api_key = _get_api_key() + if not api_key: + return False + + try: + genai_legacy.configure(api_key=api_key) + return True + except Exception as exc: + logger.warning("Failed to configure legacy Gemini SDK: %s", exc) + return False + + +def _legacy_model_candidates() -> tuple[str, ...]: + global _legacy_candidates + + if _legacy_candidates is not None: + return _legacy_candidates + + ordered = list(MODEL_CANDIDATES) + if not ENABLE_LEGACY_MODEL_DISCOVERY: + _legacy_candidates = tuple(ordered) + return _legacy_candidates + + if genai_legacy is None: + _legacy_candidates = tuple(ordered) + return _legacy_candidates + + try: + discovered: list[str] = [] + for model in genai_legacy.list_models(request_options={"timeout": REQUEST_TIMEOUT_S}): + methods = set(getattr(model, "supported_generation_methods", []) or []) + if "generateContent" not in methods: + continue + name = str(getattr(model, "name", "")).strip() + if not name: + continue + short = name.split("/", 1)[-1] + discovered.append(short) + + if discovered: + preferred = [name for name in ordered if name in discovered] + remainder = [name for name in discovered if name not in preferred] + _legacy_candidates = tuple(preferred + remainder) + else: + _legacy_candidates = tuple(ordered) + except Exception as exc: + logger.warning("Could not list Gemini models from legacy SDK: %s", exc) + _legacy_candidates = tuple(ordered) + + return _legacy_candidates + + +def _generate_with_legacy_sdk(prompt: str) -> str: + global _legacy_model, _legacy_model_name + + if not _ensure_legacy_configured(): + raise RuntimeError("legacy Gemini SDK unavailable") + + if _legacy_model is not None: + try: + response = _run_with_timeout( + lambda: _legacy_model.generate_content( + prompt, + request_options={"timeout": REQUEST_TIMEOUT_S}, + ), + REQUEST_TIMEOUT_S + 1.0, + ) + text = (getattr(response, "text", None) or "").strip() + if text: + return text + except Exception as exc: + logger.warning("Cached Gemini model %s failed: %s", _legacy_model_name, exc) + _legacy_model = None + _legacy_model_name = None + + last_error: Exception | None = None + for model_name in _legacy_model_candidates()[:MAX_MODEL_ATTEMPTS]: + try: + candidate = genai_legacy.GenerativeModel( + model_name=model_name, + system_instruction=SYSTEM_INSTRUCTION, + ) + response = _run_with_timeout( + lambda: candidate.generate_content( + prompt, + request_options={"timeout": REQUEST_TIMEOUT_S}, + ), + REQUEST_TIMEOUT_S + 1.0, + ) + text = (getattr(response, "text", None) or "").strip() + if text: + _legacy_model = candidate + _legacy_model_name = model_name + logger.info("Gemini explain model selected (legacy SDK): %s", model_name) + return text + except Exception as exc: + last_error = exc + logger.debug("Gemini model %s failed on legacy SDK: %s", model_name, exc) + + if last_error: + raise last_error + raise RuntimeError("No Gemini model succeeded via legacy SDK") + + +def explain( + verdict: str, + confidence: float, + engine_results: list[EngineResult], + generator: str, +) -> str: + breakdown = "\n".join( + f"- {result.engine}: {result.verdict} ({result.confidence:.0%}) - {result.explanation}" + for result in engine_results + ) + + prompt = ( + f"Verdict: {verdict} ({confidence:.0%} confidence)\n" + f"Attributed generator: {generator}\n" + f"Engine breakdown:\n{breakdown}\n\n" + "Write the forensics explanation." + ) + + try: + if genai_new is not None: + return _generate_with_new_sdk(prompt) + return _generate_with_legacy_sdk(prompt) + + except Exception as exc: + logger.error("Gemini explain failed: %s", exc) + top = engine_results[0] if engine_results else None + primary = f"Primary signal came from the {top.engine} engine." if top else "" + return ( + f"Content classified as {verdict} with {confidence:.0%} confidence. " + f"Attributed generator: {generator}. " + f"{primary}" + ).strip() + + +class Explainer: + """Compatibility wrapper for legacy callers expecting an object API.""" + + def explain(self, response: DetectionResponse) -> str: + return explain( + verdict=response.verdict, + confidence=response.confidence, + engine_results=response.engine_breakdown, + generator=response.attributed_generator, + ) diff --git a/src/fusion/__init__.py b/src/fusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/fusion/fuser.py b/src/fusion/fuser.py new file mode 100644 index 0000000000000000000000000000000000000000..5b0a5cc77ee669a59c059ef6b2bba39987558373 --- /dev/null +++ b/src/fusion/fuser.py @@ -0,0 +1,107 @@ +from __future__ import annotations + +import numpy as np + +from src.types import DetectionResponse, EngineResult + +ENGINE_WEIGHTS = { + "fingerprint": 0.45, + "coherence": 0.35, + "sstgnn": 0.20, +} + +ENGINE_WEIGHTS_VIDEO = { + "fingerprint": 0.30, + "coherence": 0.50, + "sstgnn": 0.20, +} + +ATTRIBUTION_PRIORITY = { + "fingerprint": 1, + "sstgnn": 2, + "coherence": 3, +} + + +def _normalize_generator(value: str | None) -> str: + if not value: + return "real" + return str(value).strip().lower().replace(" ", "_") + + +def fuse(results: list[EngineResult], is_video: bool = False) -> tuple[str, float, str]: + """Return (verdict, confidence_for_verdict, attributed_generator).""" + + weights = ENGINE_WEIGHTS_VIDEO if is_video else ENGINE_WEIGHTS + active = [result for result in results if result.verdict != "UNKNOWN"] + + if not active: + return "UNKNOWN", 0.5, "unknown_gan" + + wf = sum( + result.confidence * weights.get(result.engine, 0.1) + for result in active + if result.verdict == "FAKE" + ) + wr = sum( + (1.0 - result.confidence) * weights.get(result.engine, 0.1) + for result in active + if result.verdict == "REAL" + ) + + denom = wf + wr + 1e-9 + fake_prob = float(np.clip(wf / denom, 0.0, 1.0)) + verdict = "FAKE" if fake_prob > 0.5 else "REAL" + confidence = fake_prob if verdict == "FAKE" else (1.0 - fake_prob) + + generator = "real" + if verdict == "FAKE": + for result in sorted(active, key=lambda r: ATTRIBUTION_PRIORITY.get(r.engine, 9)): + candidate = _normalize_generator(result.attributed_generator) + if candidate and candidate != "real": + generator = candidate + break + if generator == "real": + generator = "unknown_gan" + + return verdict, confidence, generator + + +class Fuser: + """Compatibility wrapper returning `DetectionResponse` objects.""" + + def fuse( + self, + results: list[EngineResult], + media_type: str = "image", + total_ms: float = 0.0, + ) -> DetectionResponse: + if not results: + return DetectionResponse( + verdict="REAL", + confidence=0.5, + attributed_generator="unknown_gan", + explanation="No engine results available.", + processing_time_ms=round(total_ms, 2), + engine_breakdown=[], + ) + + verdict, confidence, generator = fuse(results, is_video=(media_type == "video")) + + if verdict == "UNKNOWN": + explanation = "No active engine outputs were available." + else: + summary = ", ".join( + f"{result.engine}:{result.verdict}({result.confidence:.2f})" + for result in results + ) + explanation = f"Fused {media_type} analysis from engines: {summary}." + + return DetectionResponse( + verdict=verdict, + confidence=confidence, + attributed_generator=generator, + explanation=explanation, + processing_time_ms=round(total_ms, 2), + engine_breakdown=results, + ) diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e3b4335581ad5af85d90f52c6ecd47cec338fdae --- /dev/null +++ b/src/services/__init__.py @@ -0,0 +1 @@ +# src/services — hosting / inference routing layer diff --git a/src/services/hf_inference_client.py b/src/services/hf_inference_client.py new file mode 100644 index 0000000000000000000000000000000000000000..b620311d8c2c9412a7f66cfe3bf203c3f011acbb --- /dev/null +++ b/src/services/hf_inference_client.py @@ -0,0 +1,126 @@ +""" +src/services/hf_inference_client.py — HuggingFace Serverless Inference API + +Free-tier image classification and feature extraction via +https://api-inference.huggingface.co/models/{model_id} + +No GPU required on our side; HF runs inference on shared hardware. +Rate limits: ~1000 requests/day on the free tier. + +Usage: + client = HFInferenceClient() + results = await client.classify_image(image_bytes) +""" +from __future__ import annotations + +import logging +import os +from typing import Any + +import httpx + +logger = logging.getLogger(__name__) + +_HF_API_BASE = "https://api-inference.huggingface.co" +_DEFAULT_MODEL = "Wvolf/ViT_Deepfake_Detection" + + +class HFInferenceUnavailable(RuntimeError): + """Raised when the HF Inference API is unreachable or returns an error.""" + + +class HFInferenceClient: + """ + Wraps the HuggingFace Serverless Inference REST API. + + Environment variables + --------------------- + HF_TOKEN — HuggingFace access token (required for private models, + recommended for higher rate limits on public ones) + """ + + def __init__(self, model_id: str = _DEFAULT_MODEL) -> None: + self._model_id = model_id + self._token = ( + os.environ.get("HF_TOKEN") + or os.environ.get("HuggingFaceToken") + or "" + ) + + @property + def available(self) -> bool: + return True # Public endpoint works without a token (lower rate limit) + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + async def classify_image( + self, + image_bytes: bytes, + timeout: float = 30.0, + ) -> list[dict[str, Any]]: + """ + Submit *image_bytes* for image classification. + + Returns list of dicts, e.g.: + [{"label": "FAKE", "score": 0.95}, {"label": "REAL", "score": 0.05}] + + Raises HFInferenceUnavailable on non-2xx responses. + """ + url = f"{_HF_API_BASE}/models/{self._model_id}" + headers = self._auth_headers() + headers["Content-Type"] = "application/octet-stream" + + async with httpx.AsyncClient(timeout=timeout) as client: + resp = await client.post(url, content=image_bytes, headers=headers) + + if resp.status_code == 503: + # Model is loading — common on cold start + logger.warning("HF model loading (503); you can retry in ~20s") + raise HFInferenceUnavailable( + f"HF model {self._model_id} is loading (503). Retry shortly." + ) + if not resp.is_success: + raise HFInferenceUnavailable( + f"HF Inference API returned {resp.status_code}: {resp.text[:200]}" + ) + + data = resp.json() + logger.debug("HF classify response: %s", data) + return data # type: ignore[return-value] + + async def get_features( + self, + image_bytes: bytes, + timeout: float = 30.0, + ) -> list[float]: + """ + Extract image feature embeddings via the /feature-extraction pipeline. + Returns a flat list of float values. + """ + url = f"{_HF_API_BASE}/pipeline/feature-extraction/{self._model_id}" + headers = self._auth_headers() + headers["Content-Type"] = "application/octet-stream" + + async with httpx.AsyncClient(timeout=timeout) as client: + resp = await client.post(url, content=image_bytes, headers=headers) + + if not resp.is_success: + raise HFInferenceUnavailable( + f"HF feature-extraction returned {resp.status_code}: {resp.text[:200]}" + ) + data = resp.json() + # HF returns nested list for single image — flatten one level + if isinstance(data, list) and data and isinstance(data[0], list): + return data[0] + return data # type: ignore[return-value] + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def _auth_headers(self) -> dict[str, str]: + if self._token: + return {"Authorization": f"Bearer {self._token}"} + return {} diff --git a/src/services/inference_router.py b/src/services/inference_router.py new file mode 100644 index 0000000000000000000000000000000000000000..051355fbde26310c0368b5d143c530ef0f555d8d --- /dev/null +++ b/src/services/inference_router.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import base64 +import logging +import os +from typing import Literal + +import httpx + +from src.types import DetectionResponse + +logger = logging.getLogger(__name__) +InferenceBackend = Literal["auto", "local", "runpod", "hf"] + + +def _is_placeholder(value: str) -> bool: + v = value.strip().lower() + return ( + not v + or "your_serverless_endpoint_id_here" in v + or "replace_me" in v + or "example" in v + ) + + +def get_inference_backend() -> InferenceBackend: + """ + Resolve backend mode from INFERENCE_BACKEND. + + Supported values: + - auto : local by default, route large videos to RunPod when configured + - local : always local + - runpod : always try RunPod first, then fallback in caller + - hf : Hugging Face first, then RunPod fallback (no local fallback) + """ + value = os.environ.get("INFERENCE_BACKEND", "auto").strip().lower() + if value in {"auto", "local", "runpod", "hf"}: + return value # type: ignore[return-value] + + logger.warning( + "Invalid INFERENCE_BACKEND=%r; falling back to 'auto'. " + "Expected one of: auto, local, runpod, hf.", + value, + ) + return "auto" + + +def is_runpod_configured() -> bool: + key = os.environ.get("RUNPOD_API_KEY", "").strip() + endpoint = ( + os.environ.get("RUNPOD_ENDPOINT_ID") + or os.environ.get("RUNPOD_ENDPOINT") + or "" + ).strip() + return bool(key) and not _is_placeholder(endpoint) + + +async def route_inference(data: bytes, media_type: str) -> DetectionResponse: + """Send payload to RunPod Serverless runsync endpoint.""" + + runpod_key = os.environ.get("RUNPOD_API_KEY", "").strip() + runpod_endpoint = ( + os.environ.get("RUNPOD_ENDPOINT_ID") + or os.environ.get("RUNPOD_ENDPOINT") + or "" + ).strip() + + if not is_runpod_configured(): + raise RuntimeError( + "RunPod not configured. Set valid RUNPOD_API_KEY and RUNPOD_ENDPOINT_ID." + ) + + url = f"https://api.runpod.ai/v2/{runpod_endpoint}/runsync" + payload = { + "input": { + "data": base64.b64encode(data).decode("utf-8"), + "media_type": media_type, + } + } + + headers = { + "Authorization": f"Bearer {runpod_key}", + "Content-Type": "application/json", + } + + async with httpx.AsyncClient(timeout=120.0) as client: + response = await client.post(url, json=payload, headers=headers) + response.raise_for_status() + + body = response.json() + output = body.get("output") + if output is None: + raise RuntimeError(f"RunPod response missing output: {body}") + + return DetectionResponse(**output) + + +class InferenceRouter: + """Compatibility wrapper for older router callers.""" + + async def route(self, data: bytes, media_type: str) -> DetectionResponse: + return await route_inference(data, media_type) diff --git a/src/services/runpod_client.py b/src/services/runpod_client.py new file mode 100644 index 0000000000000000000000000000000000000000..cc8bb098d4ee3c37aa7ebf67b22a5300d13ee175 --- /dev/null +++ b/src/services/runpod_client.py @@ -0,0 +1,132 @@ +""" +src/services/runpod_client.py — RunPod Serverless inference client + +Submits image/frame payloads to a RunPod Serverless endpoint and polls for +results. Falls back gracefully when RUNPOD_API_KEY or RUNPOD_ENDPOINT_ID +are unset. + +Usage: + client = RunPodClient() + result = await client.classify_image(image_bytes) +""" +from __future__ import annotations + +import asyncio +import base64 +import logging +import os +import time +from typing import Any, Optional + +import httpx + +logger = logging.getLogger(__name__) + +_API_BASE = "https://api.runpod.ai/v2" +_POLL_INTERVAL_SEC = 1.0 +_MAX_POLL_SECONDS = 55 # Keep under typical 60-s Lambda timeout + + +class RunPodUnavailable(RuntimeError): + """Raised when RunPod is not configured or returns a non-retryable error.""" + + +class RunPodClient: + """ + Thin async client for RunPod Serverless endpoints. + + Environment variables + --------------------- + RUNPOD_API_KEY — RunPod API key (rpa_…) + RUNPOD_ENDPOINT_ID — ID of the deployed serverless endpoint + RUNPOD_MODEL_ID — HF model ID used inside the RunPod worker (informational) + """ + + def __init__(self) -> None: + self._api_key = os.environ.get("RUNPOD_API_KEY", "") + self._endpoint_id = os.environ.get("RUNPOD_ENDPOINT_ID", "") + self._model_id = os.environ.get("RUNPOD_MODEL_ID", "Wvolf/ViT_Deepfake_Detection") + + @property + def available(self) -> bool: + """True when both API key and endpoint ID are configured.""" + return bool(self._api_key and self._endpoint_id) + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + async def classify_image( + self, + image_bytes: bytes, + timeout: float = _MAX_POLL_SECONDS, + ) -> dict[str, Any]: + """ + Send *image_bytes* to the RunPod endpoint and return the worker result. + + Returns a dict like: + {"verdict": "FAKE", "confidence": 0.95, "generator": "stable_diffusion"} + + Raises RunPodUnavailable if not configured or on persistent failure. + """ + if not self.available: + raise RunPodUnavailable( + "RunPod not configured: set RUNPOD_API_KEY and RUNPOD_ENDPOINT_ID" + ) + + b64 = base64.b64encode(image_bytes).decode() + payload = { + "input": { + "image_b64": b64, + "model_id": self._model_id, + } + } + + job_id = await self._submit(payload) + logger.info("RunPod job submitted: %s", job_id) + return await self._poll(job_id, timeout=timeout) + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + async def _submit(self, payload: dict[str, Any]) -> str: + url = f"{_API_BASE}/{self._endpoint_id}/run" + headers = { + "Authorization": f"Bearer {self._api_key}", + "Content-Type": "application/json", + } + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.post(url, json=payload, headers=headers) + resp.raise_for_status() + data = resp.json() + if "id" not in data: + raise RunPodUnavailable(f"Unexpected RunPod response: {data}") + return data["id"] + + async def _poll(self, job_id: str, timeout: float) -> dict[str, Any]: + url = f"{_API_BASE}/{self._endpoint_id}/status/{job_id}" + headers = {"Authorization": f"Bearer {self._api_key}"} + deadline = time.monotonic() + timeout + + async with httpx.AsyncClient(timeout=10) as client: + while time.monotonic() < deadline: + resp = await client.get(url, headers=headers) + resp.raise_for_status() + data = resp.json() + status = data.get("status", "") + + if status == "COMPLETED": + output = data.get("output") or {} + logger.info("RunPod job %s completed: %s", job_id, output) + return output # type: ignore[return-value] + + if status in ("FAILED", "CANCELLED"): + err = data.get("error", "unknown RunPod error") + raise RunPodUnavailable(f"RunPod job {job_id} {status}: {err}") + + await asyncio.sleep(_POLL_INTERVAL_SEC) + + raise RunPodUnavailable( + f"RunPod job {job_id} did not complete within {timeout}s" + ) diff --git a/src/training/__init__.py b/src/training/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/training/config.py b/src/training/config.py new file mode 100644 index 0000000000000000000000000000000000000000..eb2bbe1bf86f7f13bd0da4cbff211826011ab40c --- /dev/null +++ b/src/training/config.py @@ -0,0 +1,150 @@ +""" +src/training/config.py — TrainingConfig dataclass + +Single source of truth for all training hyperparameters. Import this from +every training script instead of duplicating argparse defaults. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import List + + +# Generator label index mapping — must match GeneratorLabel enum in src/types.py +# and the classification head in every model file. +GENERATOR_CLASSES: List[str] = [ + "real", # 0 + "unknown_gan", # 1 + "stable_diffusion", # 2 + "midjourney", # 3 + "dall_e", # 4 + "flux", # 5 + "firefly", # 6 + "imagen", # 7 +] +NUM_GENERATOR_CLASSES: int = len(GENERATOR_CLASSES) # 8 — never change this + + +@dataclass +class TrainingConfig: + """Fingerprint engine training configuration (Phase 1).""" + + # ── Paths ───────────────────────────────────────────────────────────────── + data_dir: Path = Path("data/processed/fingerprint") + output_dir: Path = Path("models/checkpoints/fingerprint") + log_dir: Path = Path("training/logs") + + # ── Model ───────────────────────────────────────────────────────────────── + model_name: str = "vit_base_patch16_224" # timm slug + pretrained: bool = True + num_binary_classes: int = 2 + num_generator_classes: int = NUM_GENERATOR_CLASSES # 8 + + # ── Training ────────────────────────────────────────────────────────────── + epochs: int = 30 + batch_size: int = 64 + learning_rate: float = 2e-5 + weight_decay: float = 0.01 + warmup_steps: int = 500 + grad_accumulation: int = 1 + amp: bool = True + generator_loss_weight: float = 0.3 # secondary objective weight + + # ── Optimiser ───────────────────────────────────────────────────────────── + optimizer: str = "adamw" + scheduler: str = "cosine" + + # ── Early stopping ──────────────────────────────────────────────────────── + patience: int = 5 # stop if val AUC flat for N epochs + + # ── Reproducibility ─────────────────────────────────────────────────────── + seed: int = 42 + + # ── Kaggle ──────────────────────────────────────────────────────────────── + is_kaggle: bool = False + + def __post_init__(self) -> None: + self.data_dir = Path(self.data_dir) + self.output_dir = Path(self.output_dir) + self.log_dir = Path(self.log_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.log_dir.mkdir(parents=True, exist_ok=True) + + def to_dict(self) -> dict: + return { + k: str(v) if isinstance(v, Path) else v + for k, v in self.__dict__.items() + } + + +@dataclass +class CoherenceConfig: + """Coherence engine training configuration (Phase 2).""" + + data_dir: Path = Path("data/processed/coherence") + output_dir: Path = Path("models/checkpoints/coherence") + log_dir: Path = Path("training/logs") + + epochs: int = 25 + batch_size: int = 16 + learning_rate: float = 1e-4 + weight_decay: float = 1e-4 + contrastive_weight: float = 0.1 + amp: bool = True + patience: int = 5 + seed: int = 42 + is_kaggle: bool = False + + def __post_init__(self) -> None: + self.data_dir = Path(self.data_dir) + self.output_dir = Path(self.output_dir) + self.log_dir = Path(self.log_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.log_dir.mkdir(parents=True, exist_ok=True) + + def to_dict(self) -> dict: + return { + k: str(v) if isinstance(v, Path) else v + for k, v in self.__dict__.items() + } + + +@dataclass +class SSTGNNConfig: + """SSTGNN engine training configuration (Phase 3).""" + + data_dir: Path = Path("data/processed/sstgnn") + output_dir: Path = Path("models/checkpoints/sstgnn") + log_dir: Path = Path("training/logs") + + # Graph model + in_channels: int = 5 # node feature dim: x, y, z, frame_idx/T, lm_idx/L + hidden_dim: int = 64 + heads: int = 4 + num_gat_layers: int = 3 + + # Training — note: no AMP by default (NaN risk on GNNs) + epochs: int = 40 + batch_size: int = 8 + learning_rate: float = 5e-4 + weight_decay: float = 5e-4 + grad_clip: float = 1.0 # always clip for GNNs + amp: bool = False + patience: int = 8 # GNNs need longer to plateau + seed: int = 42 + is_kaggle: bool = False + + def __post_init__(self) -> None: + self.data_dir = Path(self.data_dir) + self.output_dir = Path(self.output_dir) + self.log_dir = Path(self.log_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.log_dir.mkdir(parents=True, exist_ok=True) + + def to_dict(self) -> dict: + return { + k: str(v) if isinstance(v, Path) else v + for k, v in self.__dict__.items() + } diff --git a/src/training/datasets.py b/src/training/datasets.py new file mode 100644 index 0000000000000000000000000000000000000000..2363e45b11b9222cc68c36a64635f4bb3038882a --- /dev/null +++ b/src/training/datasets.py @@ -0,0 +1,91 @@ +""" +src/training/datasets.py + +Shared dataset utilities used by scripts/ entrypoints. +""" +from __future__ import annotations + +import csv +from pathlib import Path +from typing import Optional + +import numpy as np +import torch +from PIL import Image +from torch.utils.data import Dataset +from torchvision import transforms + +IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp"} + +IMAGENET_MEAN = [0.485, 0.456, 0.406] +IMAGENET_STD = [0.229, 0.224, 0.225] + + +def get_train_transform(size: int = 224): + return transforms.Compose([ + transforms.RandomResizedCrop(size, scale=(0.8, 1.0)), + transforms.RandomHorizontalFlip(), + transforms.ColorJitter(0.2, 0.2, 0.2, 0.1), + transforms.ToTensor(), + transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), + ]) + + +def get_val_transform(size: int = 224): + return transforms.Compose([ + transforms.Resize(int(size * 256 / 224)), + transforms.CenterCrop(size), + transforms.ToTensor(), + transforms.Normalize(IMAGENET_MEAN, IMAGENET_STD), + ]) + + +class ImageManifestDataset(Dataset): + """ + Generic image dataset driven by a manifest CSV. + + Manifest format: filepath, label (0=real, 1=fake), [generator (int)] + """ + + def __init__( + self, + manifest_path: Path, + transform=None, + root_dir: Optional[Path] = None, + ): + self.transform = transform + self.root_dir = Path(root_dir) if root_dir else None + self.samples = [] + + with open(manifest_path) as f: + reader = csv.DictReader(f) + for row in reader: + filepath = Path(row["filepath"]) + if self.root_dir and not filepath.is_absolute(): + filepath = self.root_dir / filepath + label = int(row["label"]) + generator = int(row.get("generator", 0)) + self.samples.append((filepath, label, generator)) + + def __len__(self) -> int: + return len(self.samples) + + def __getitem__(self, idx: int) -> dict: + path, label, generator = self.samples[idx] + img = Image.open(path).convert("RGB") + if self.transform: + img = self.transform(img) + return { + "image": img, + "label": label, + "generator": generator, + "filepath": str(path), + } + + def get_class_weights(self) -> torch.Tensor: + labels = [s[1] for s in self.samples] + n_real = labels.count(0) + n_fake = labels.count(1) + w_real = 1.0 / max(n_real, 1) + w_fake = 1.0 / max(n_fake, 1) + return torch.tensor([w_real, w_fake], dtype=torch.float32) diff --git a/src/training/image_trainer.py b/src/training/image_trainer.py new file mode 100644 index 0000000000000000000000000000000000000000..bac51bb7ff21e5e469203eaf9f641856b12b750b --- /dev/null +++ b/src/training/image_trainer.py @@ -0,0 +1,149 @@ +""" +src/training/image_trainer.py + +Reusable Trainer class wrapping the Fingerprint engine training loop. +Used by scripts/train_image_baseline.py. +""" +from __future__ import annotations + +import logging +import time +from pathlib import Path +from typing import Optional + +import numpy as np +import torch +import torch.nn as nn +from torch.cuda.amp import GradScaler, autocast +from torch.utils.data import DataLoader +from sklearn.metrics import roc_auc_score, accuracy_score + +from src.training.config import TrainingConfig + +log = logging.getLogger(__name__) + + +class ImageTrainer: + """ + Encapsulates the training loop for image-based classification models. + + Usage: + trainer = ImageTrainer(model, config, train_loader, val_loader, device) + trainer.train() + """ + + def __init__( + self, + model: nn.Module, + config: TrainingConfig, + train_loader: DataLoader, + val_loader: DataLoader, + device: torch.device, + criterion: Optional[nn.Module] = None, + ): + self.model = model.to(device) + self.config = config + self.train_loader = train_loader + self.val_loader = val_loader + self.device = device + self.criterion = criterion or nn.CrossEntropyLoss() + + self.optimizer = torch.optim.AdamW( + model.parameters(), + lr=config.learning_rate, + weight_decay=config.weight_decay, + ) + self.scaler = GradScaler(enabled=config.amp) + + self.best_auc = 0.0 + self.no_improve = 0 + self.output_dir = Path(config.output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + + def train(self) -> float: + """Run full training. Returns best val AUC.""" + log.info(f"Training for {self.config.epochs} epochs on {self.device}") + + for epoch in range(self.config.epochs): + t0 = time.time() + train_loss = self._train_epoch(epoch) + val_metrics = self._validate() + + log.info( + f"Epoch {epoch+1:02d}/{self.config.epochs} | " + f"train_loss={train_loss:.4f} | " + f"val_auc={val_metrics['auc']:.4f} | " + f"val_acc={val_metrics['accuracy']:.4f} | " + f"{time.time()-t0:.0f}s" + ) + + self._save("latest.pt", epoch, val_metrics["auc"]) + + if val_metrics["auc"] > self.best_auc: + self.best_auc = val_metrics["auc"] + self.no_improve = 0 + self._save("best.pt", epoch, self.best_auc) + log.info(f" New best AUC: {self.best_auc:.4f}") + else: + self.no_improve += 1 + if self.no_improve >= self.config.patience: + log.info(f"Early stopping at epoch {epoch+1}") + break + + return self.best_auc + + def _train_epoch(self, epoch: int) -> float: + self.model.train() + total_loss = 0.0 + + for i, batch in enumerate(self.train_loader): + images = batch["image"].to(self.device) + labels = batch["label"].to(self.device) + + with autocast(enabled=self.config.amp): + outputs = self.model(images) + logits = outputs.get("binary_logits", outputs) if isinstance(outputs, dict) else outputs + loss = self.criterion(logits, labels) / self.config.grad_accumulation + + self.scaler.scale(loss).backward() + + if (i + 1) % self.config.grad_accumulation == 0: + self.scaler.unscale_(self.optimizer) + nn.utils.clip_grad_norm_(self.model.parameters(), 1.0) + self.scaler.step(self.optimizer) + self.scaler.update() + self.optimizer.zero_grad() + + total_loss += loss.item() * self.config.grad_accumulation + + return total_loss / len(self.train_loader) + + @torch.no_grad() + def _validate(self) -> dict: + self.model.eval() + all_probs, all_labels = [], [] + + for batch in self.val_loader: + images = batch["image"].to(self.device) + labels = batch["label"] + + outputs = self.model(images) + logits = outputs.get("binary_logits", outputs) if isinstance(outputs, dict) else outputs + probs = torch.softmax(logits, dim=1)[:, 1].cpu().numpy() + + all_probs.extend(probs) + all_labels.extend(labels.numpy()) + + auc = roc_auc_score(all_labels, all_probs) if len(set(all_labels)) > 1 else 0.5 + acc = accuracy_score(all_labels, (np.array(all_probs) > 0.5).astype(int)) + return {"auc": auc, "accuracy": acc} + + def _save(self, filename: str, epoch: int, val_auc: float) -> None: + torch.save({ + "epoch": epoch, + "model_state": self.model.state_dict(), + "optimizer": self.optimizer.state_dict(), + "scaler": self.scaler.state_dict(), + "val_auc": val_auc, + "config": self.config.to_dict(), + }, self.output_dir / filename) diff --git a/src/training/manifests.py b/src/training/manifests.py new file mode 100644 index 0000000000000000000000000000000000000000..60e1f7abb481af0942485c38d1f03cc0c65a8a6f --- /dev/null +++ b/src/training/manifests.py @@ -0,0 +1,76 @@ +"""src/training/manifests.py — Build and validate manifest CSV files.""" +from __future__ import annotations + +import csv +import random +from pathlib import Path +from typing import List, Tuple + +IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp"} + + +def build_manifest( + image_dirs: List[Tuple[Path, int, int]], # (dir, label, generator_idx) + output_path: Path, + seed: int = 42, + train_ratio: float = 0.80, + val_ratio: float = 0.10, +) -> dict: + """ + Walk image directories, build split manifests. + + Returns dict with train/val/test paths. + """ + rng = random.Random(seed) + records = [] + + for img_dir, label, generator in image_dirs: + for p in sorted(Path(img_dir).rglob("*")): + if p.suffix.lower() in IMAGE_EXTS: + records.append({ + "filepath": str(p), + "label": label, + "generator": generator, + }) + + rng.shuffle(records) + n = len(records) + n_train = int(n * train_ratio) + n_val = int(n * val_ratio) + + splits = { + "train": records[:n_train], + "val": records[n_train:n_train + n_val], + "test": records[n_train + n_val:], + } + + output_path.parent.mkdir(parents=True, exist_ok=True) + manifest_paths = {} + + for split, rows in splits.items(): + out = output_path.parent / f"{output_path.stem}_{split}.csv" + with open(out, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=["filepath", "label", "generator"]) + writer.writeheader() + writer.writerows(rows) + manifest_paths[split] = out + + return manifest_paths + + +def validate_manifest(manifest_path: Path) -> dict: + """Check a manifest CSV is well-formed and all files exist.""" + missing = [] + counts = {"total": 0, "real": 0, "fake": 0} + + with open(manifest_path) as f: + for row in csv.DictReader(f): + counts["total"] += 1 + if int(row["label"]) == 0: + counts["real"] += 1 + else: + counts["fake"] += 1 + if not Path(row["filepath"]).exists(): + missing.append(row["filepath"]) + + return {"counts": counts, "missing": missing, "ok": len(missing) == 0} diff --git a/src/training/metrics.py b/src/training/metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..f51fe9d7f8a4f1b3da78089eda680f2ea27f62f4 --- /dev/null +++ b/src/training/metrics.py @@ -0,0 +1,50 @@ +"""src/training/metrics.py — Shared metric computation utilities.""" +from __future__ import annotations + +import numpy as np +from sklearn.metrics import ( + roc_auc_score, f1_score, accuracy_score, + precision_score, recall_score, average_precision_score, + roc_curve, +) + + +def compute_auc(y_true: list, y_scores: list) -> float: + if len(set(y_true)) < 2: + return 0.5 + return float(roc_auc_score(y_true, y_scores)) + + +def compute_eer(y_true: list, y_scores: list) -> float: + if len(set(y_true)) < 2: + return 0.5 + fpr, tpr, _ = roc_curve(y_true, y_scores) + idx = int(np.argmin(np.abs(fpr - (1 - tpr)))) + return float(fpr[idx]) + + +def optimal_threshold(y_true: list, y_scores: list) -> float: + """Threshold that maximises F1 on the provided set.""" + from sklearn.metrics import precision_recall_curve + precision, recall, thresholds = precision_recall_curve(y_true, y_scores) + f1 = 2 * precision * recall / (precision + recall + 1e-8) + best = int(np.argmax(f1[:-1])) + return float(thresholds[best]) + + +def compute_all( + y_true: list, + y_scores: list, + threshold: float = 0.5, +) -> dict: + y_pred = (np.array(y_scores) >= threshold).astype(int) + return { + "auc": compute_auc(y_true, y_scores), + "auc_pr": float(average_precision_score(y_true, y_scores)) if len(set(y_true)) > 1 else 0.5, + "eer": compute_eer(y_true, y_scores), + "f1": float(f1_score(y_true, y_pred, zero_division=0)), + "accuracy": float(accuracy_score(y_true, y_pred)), + "precision": float(precision_score(y_true, y_pred, zero_division=0)), + "recall": float(recall_score(y_true, y_pred, zero_division=0)), + "threshold": threshold, + } diff --git a/src/types.py b/src/types.py new file mode 100644 index 0000000000000000000000000000000000000000..ebaeb3cf03dd578169ee39dd578fb856c80a1d3f --- /dev/null +++ b/src/types.py @@ -0,0 +1,84 @@ +""" +Shared API contracts for GenAI-DeepDetect. + +These types are used by API routes, fusion, explainability, and frontend clients. +""" +from __future__ import annotations + +from enum import Enum +from typing import Literal, Optional + +from pydantic import BaseModel, field_validator + + +class GeneratorLabel(str, Enum): + """Generator attribution labels used across the pipeline.""" + + real = "real" + unknown_gan = "unknown_gan" + stable_diffusion = "stable_diffusion" + midjourney = "midjourney" + dall_e = "dall_e" + flux = "flux" + firefly = "firefly" + imagen = "imagen" + + +GENERATOR_INDEX_TO_LABEL: dict[int, GeneratorLabel] = { + 0: GeneratorLabel.real, + 1: GeneratorLabel.unknown_gan, + 2: GeneratorLabel.stable_diffusion, + 3: GeneratorLabel.midjourney, + 4: GeneratorLabel.dall_e, + 5: GeneratorLabel.flux, + 6: GeneratorLabel.firefly, + 7: GeneratorLabel.imagen, +} + + +class EngineResult(BaseModel): + """Single-engine inference output.""" + + engine: str + verdict: Literal["FAKE", "REAL", "UNKNOWN"] + confidence: float + attributed_generator: Optional[str] = None + explanation: str = "" + processing_time_ms: float = 0.0 + + @field_validator("confidence") + @classmethod + def confidence_in_range(cls, value: float) -> float: + if not 0.0 <= value <= 1.0: + raise ValueError(f"confidence must be in [0,1], got {value}") + return round(float(value), 4) + + @field_validator("processing_time_ms") + @classmethod + def processing_time_non_negative(cls, value: float) -> float: + if value < 0: + raise ValueError("processing_time_ms must be >= 0") + return round(float(value), 2) + + +class DetectionResponse(BaseModel): + """Unified API response for image/video detection endpoints.""" + + verdict: Literal["FAKE", "REAL", "UNKNOWN"] + confidence: float + attributed_generator: str + explanation: str + processing_time_ms: float + engine_breakdown: list[EngineResult] + + # Optional explainability metadata + clarity_score: Optional[float] = None + saliency_map_url: Optional[str] = None + structured_explanation: Optional[dict] = None + + @field_validator("confidence") + @classmethod + def response_confidence_in_range(cls, value: float) -> float: + if not 0.0 <= value <= 1.0: + raise ValueError(f"confidence must be in [0,1], got {value}") + return round(float(value), 4) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..d0aa4ec96b8548af85d69979f3163b9654031f35 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,70 @@ +"""tests/conftest.py — shared pytest fixtures""" +from __future__ import annotations + +import io +import sys +from pathlib import Path + +import numpy as np +import pytest +from PIL import Image + +# Ensure repo root is on path +sys.path.insert(0, str(Path(__file__).parent.parent)) + + +@pytest.fixture +def fake_image_bytes() -> bytes: + """Returns JPEG bytes of a random 224×224 RGB image.""" + arr = (np.random.rand(224, 224, 3) * 255).astype(np.uint8) + img = Image.fromarray(arr) + buf = io.BytesIO() + img.save(buf, format="JPEG") + return buf.getvalue() + + +@pytest.fixture +def fake_image_bytes_png() -> bytes: + arr = (np.random.rand(224, 224, 3) * 255).astype(np.uint8) + img = Image.fromarray(arr) + buf = io.BytesIO() + img.save(buf, format="PNG") + return buf.getvalue() + + +@pytest.fixture +def fake_landmark_sequence() -> "np.ndarray": + """Returns (64, 68, 3) float32 landmark array with random values in [0,1].""" + return np.random.rand(64, 68, 3).astype(np.float32) + + +@pytest.fixture +def fake_lip_mfcc(): + """Returns (lip_frames, mfcc) as numpy arrays.""" + lip = np.random.rand(25, 64, 96, 3).astype(np.float32) + mfcc = np.random.rand(40, 50).astype(np.float32) + return lip, mfcc + + +@pytest.fixture +def engine_result_fake(): + from src.types import EngineResult + return EngineResult( + engine="fingerprint", + verdict="FAKE", + confidence=0.92, + explanation="Test explanation.", + processing_time_ms=50.0, + ) + + +@pytest.fixture +def engine_result_real(): + from src.types import EngineResult + return EngineResult( + engine="fingerprint", + verdict="REAL", + confidence=0.85, + explanation="No synthetic fingerprints found.", + processing_time_ms=50.0, + ) diff --git a/tests/engines/__init__.py b/tests/engines/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/engines/test_coherence.py b/tests/engines/test_coherence.py new file mode 100644 index 0000000000000000000000000000000000000000..a516477b5b5902d16bd55af04e3956fa01a07953 --- /dev/null +++ b/tests/engines/test_coherence.py @@ -0,0 +1,35 @@ +"""tests/engines/test_coherence.py""" +from __future__ import annotations + +import pytest +from src.types import EngineResult + + +def test_coherence_detector_imports(): + from src.engines.coherence.detector import CoherenceDetector + assert CoherenceDetector is not None + + +def test_coherence_image_stub(): + from src.engines.coherence.detector import CoherenceDetector + stub = CoherenceDetector.image_stub() + assert isinstance(stub, EngineResult) + assert stub.engine == "coherence" + assert stub.verdict in ("FAKE", "REAL") + assert "N/A" in stub.explanation + assert stub.processing_time_ms == 0.0 + + +def test_coherence_detector_init(): + from src.engines.coherence.detector import CoherenceDetector + det = CoherenceDetector() + assert 0.0 < det.threshold <= 1.0 + + +def test_coherence_error_result_is_valid(): + from src.engines.coherence.detector import CoherenceDetector + det = CoherenceDetector() + result = det._error_result(0.05) + assert isinstance(result, EngineResult) + assert result.verdict in ("FAKE", "REAL") + assert 0.0 <= result.confidence <= 1.0 diff --git a/tests/engines/test_fingerprint.py b/tests/engines/test_fingerprint.py new file mode 100644 index 0000000000000000000000000000000000000000..21d3127d5a50f0ba695435d7cdea81a93a25e8bf --- /dev/null +++ b/tests/engines/test_fingerprint.py @@ -0,0 +1,52 @@ +"""tests/engines/test_fingerprint.py""" +from __future__ import annotations + +import pytest +from src.types import EngineResult + + +def test_fingerprint_detector_imports(): + from src.engines.fingerprint.detector import FingerprintDetector + assert FingerprintDetector is not None + + +def test_fingerprint_detector_init(): + from src.engines.fingerprint.detector import FingerprintDetector + det = FingerprintDetector() + assert det.model is not None + assert 0.0 < det.threshold <= 1.0 + + +def test_fingerprint_detect_bytes_returns_engine_result(fake_image_bytes): + from src.engines.fingerprint.detector import FingerprintDetector + det = FingerprintDetector() + result = det.detect_bytes(fake_image_bytes) + assert isinstance(result, EngineResult) + assert result.engine == "fingerprint" + assert result.verdict in ("FAKE", "REAL") + assert 0.0 <= result.confidence <= 1.0 + assert result.processing_time_ms > 0 + assert len(result.explanation) > 0 + + +def test_fingerprint_result_confidence_range(fake_image_bytes): + from src.engines.fingerprint.detector import FingerprintDetector + det = FingerprintDetector() + result = det.detect_bytes(fake_image_bytes) + assert result.confidence >= 0.0 + assert result.confidence <= 1.0 + + +def test_fingerprint_image_stub(): + from src.engines.fingerprint.detector import FingerprintDetector + stub = FingerprintDetector.image_stub() + assert isinstance(stub, EngineResult) + + +def test_fingerprint_handles_corrupt_bytes(): + from src.engines.fingerprint.detector import FingerprintDetector + det = FingerprintDetector() + result = det.detect_bytes(b"not an image") + # Must not raise — should return error result + assert isinstance(result, EngineResult) + assert result.verdict in ("FAKE", "REAL") diff --git a/tests/engines/test_sstgnn.py b/tests/engines/test_sstgnn.py new file mode 100644 index 0000000000000000000000000000000000000000..b2101941b4b2ff0d549c0a6827f3a5a58a2f07c9 --- /dev/null +++ b/tests/engines/test_sstgnn.py @@ -0,0 +1,58 @@ +"""tests/engines/test_sstgnn.py""" +from __future__ import annotations + +import numpy as np +import pytest +from src.types import EngineResult + + +def test_sstgnn_detector_imports(): + from src.engines.sstgnn.detector import SSTGNNDetector + assert SSTGNNDetector is not None + + +def test_sstgnn_image_stub(): + from src.engines.sstgnn.detector import SSTGNNDetector + stub = SSTGNNDetector.image_stub() + assert isinstance(stub, EngineResult) + assert stub.engine == "sstgnn" + assert stub.verdict in ("FAKE", "REAL") + assert "N/A" in stub.explanation + + +def test_sstgnn_detector_init(): + from src.engines.sstgnn.detector import SSTGNNDetector + det = SSTGNNDetector() + assert 0.0 < det.threshold <= 1.0 + + +def test_sstgnn_heuristic_fallback(fake_landmark_sequence): + """Heuristic detect should work without a trained model.""" + from src.engines.sstgnn.detector import SSTGNNDetector + det = SSTGNNDetector() + t0 = 0.0 + result = det._detect_heuristic(fake_landmark_sequence, t0) + assert isinstance(result, EngineResult) + assert result.verdict in ("FAKE", "REAL") + assert 0.0 <= result.confidence <= 1.0 + + +def test_graph_builder_output_shape(fake_landmark_sequence): + """Graph builder must produce correct node/edge shapes.""" + pytest.importorskip("torch_geometric") + from src.engines.sstgnn.graph_builder import build_temporal_graph + + graph = build_temporal_graph(fake_landmark_sequence, label=1) + T, L, _ = fake_landmark_sequence.shape + assert graph.x.shape == (T * L, 5) + assert graph.edge_index.shape[0] == 2 + assert graph.edge_attr.shape[1] == 1 + assert graph.y is not None + + +def test_graph_builder_label_minus_one(fake_landmark_sequence): + """label=-1 should not set graph.y.""" + pytest.importorskip("torch_geometric") + from src.engines.sstgnn.graph_builder import build_temporal_graph + graph = build_temporal_graph(fake_landmark_sequence, label=-1) + assert not hasattr(graph, "y") or graph.y is None diff --git a/tests/services/test_inference_router.py b/tests/services/test_inference_router.py new file mode 100644 index 0000000000000000000000000000000000000000..d97fbc91c27df0667bd55205e7a4ca634068da31 --- /dev/null +++ b/tests/services/test_inference_router.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from src.services.inference_router import get_inference_backend, is_runpod_configured + + +def test_get_inference_backend_defaults_to_auto(monkeypatch): + monkeypatch.delenv("INFERENCE_BACKEND", raising=False) + assert get_inference_backend() == "auto" + + +def test_get_inference_backend_accepts_valid_values(monkeypatch): + monkeypatch.setenv("INFERENCE_BACKEND", "local") + assert get_inference_backend() == "local" + + monkeypatch.setenv("INFERENCE_BACKEND", "runpod") + assert get_inference_backend() == "runpod" + + monkeypatch.setenv("INFERENCE_BACKEND", "hf") + assert get_inference_backend() == "hf" + + monkeypatch.setenv("INFERENCE_BACKEND", "auto") + assert get_inference_backend() == "auto" + + +def test_get_inference_backend_invalid_value_falls_back_to_auto(monkeypatch): + monkeypatch.setenv("INFERENCE_BACKEND", "invalid_mode") + assert get_inference_backend() == "auto" + + +def test_is_runpod_configured_false_on_placeholder_endpoint(monkeypatch): + monkeypatch.setenv("RUNPOD_API_KEY", "rpa_test") + monkeypatch.setenv("RUNPOD_ENDPOINT_ID", "your_serverless_endpoint_id_here") + assert is_runpod_configured() is False + + +def test_is_runpod_configured_true_when_key_and_endpoint_set(monkeypatch): + monkeypatch.setenv("RUNPOD_API_KEY", "rpa_test") + monkeypatch.setenv("RUNPOD_ENDPOINT_ID", "abcd1234") + assert is_runpod_configured() is True diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000000000000000000000000000000000000..a68bed15298c7673f7cd7d0bb3a33f6791148f5c --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,136 @@ +"""tests/test_api.py — FastAPI endpoint integration tests.""" +from __future__ import annotations + +import io +import pytest +import numpy as np +from PIL import Image +from fastapi.testclient import TestClient + + +@pytest.fixture(scope="module") +def client(): + from src.api.main import app + return TestClient(app) + + +@pytest.fixture +def jpeg_bytes(): + arr = (np.random.rand(224, 224, 3) * 255).astype(np.uint8) + buf = io.BytesIO() + Image.fromarray(arr).save(buf, format="JPEG") + return buf.getvalue() + + +# ── GET /health ─────────────────────────────────────────────────────────────── + +def test_health_returns_200(client): + r = client.get("/health") + assert r.status_code == 200 + + +def test_health_has_required_fields(client): + data = client.get("/health").json() + assert data["status"] == "ok" + assert "version" in data + assert "engines" in data + assert "inference_backend" in data + assert "runpod_configured" in data + assert set(data["engines"]) == {"fingerprint", "coherence", "sstgnn"} + + +def test_health_models_returns_inventory(client): + data = client.get("/health/models").json() + assert "fingerprint" in data + assert "coherence" in data + assert "sstgnn" in data + assert "generator_labels" in data + assert "stable_diffusion" in data["generator_labels"] + + +# ── GET / ───────────────────────────────────────────────────────────────────── + +def test_root_returns_html(client): + r = client.get("/") + assert r.status_code == 200 + assert "text/html" in r.headers["content-type"] + + +# ── POST /detect/image ──────────────────────────────────────────────────────── + +def test_detect_image_returns_200(client, jpeg_bytes): + r = client.post( + "/detect/image", + files={"file": ("test.jpg", jpeg_bytes, "image/jpeg")}, + ) + assert r.status_code == 200 + + +def test_detect_image_response_schema(client, jpeg_bytes): + data = client.post( + "/detect/image", + files={"file": ("test.jpg", jpeg_bytes, "image/jpeg")}, + ).json() + + assert data["verdict"] in ("FAKE", "REAL") + assert 0.0 <= data["confidence"] <= 1.0 + assert "attributed_generator" in data + assert "explanation" in data + assert "engine_breakdown" in data + assert len(data["engine_breakdown"]) == 3 + + +def test_detect_image_engine_names(client, jpeg_bytes): + data = client.post( + "/detect/image", + files={"file": ("test.jpg", jpeg_bytes, "image/jpeg")}, + ).json() + + engine_names = {e["engine"] for e in data["engine_breakdown"]} + assert engine_names == {"fingerprint", "coherence", "sstgnn"} + + +def test_detect_image_engine_confidence_range(client, jpeg_bytes): + data = client.post( + "/detect/image", + files={"file": ("test.jpg", jpeg_bytes, "image/jpeg")}, + ).json() + + for engine in data["engine_breakdown"]: + assert 0.0 <= engine["confidence"] <= 1.0 + assert engine["verdict"] in ("FAKE", "REAL") + + +def test_detect_image_too_large_returns_413(client): + big = b"x" * (21 * 1024 * 1024) # 21MB > 20MB limit + r = client.post( + "/detect/image", + files={"file": ("big.jpg", big, "image/jpeg")}, + ) + assert r.status_code == 413 + + +def test_detect_image_wrong_type_returns_415(client, jpeg_bytes): + r = client.post( + "/detect/image", + files={"file": ("test.mp4", jpeg_bytes, "video/mp4")}, + ) + assert r.status_code == 415 + + +def test_detect_image_processing_time_positive(client, jpeg_bytes): + data = client.post( + "/detect/image", + files={"file": ("test.jpg", jpeg_bytes, "image/jpeg")}, + ).json() + assert data["processing_time_ms"] >= 0 + + +# ── POST /detect/video ──────────────────────────────────────────────────────── + +def test_detect_video_wrong_type_returns_415(client, jpeg_bytes): + r = client.post( + "/detect/video", + files={"file": ("test.jpg", jpeg_bytes, "image/jpeg")}, + ) + assert r.status_code == 415 diff --git a/tests/test_fusion.py b/tests/test_fusion.py new file mode 100644 index 0000000000000000000000000000000000000000..fb2605855988b39cca102444a7a90eb40d662316 --- /dev/null +++ b/tests/test_fusion.py @@ -0,0 +1,100 @@ +"""tests/test_fusion.py""" +from __future__ import annotations + +import pytest +from src.types import EngineResult, GeneratorLabel + + +def _make_result(engine, verdict, conf, explanation="Test"): + return EngineResult( + engine=engine, verdict=verdict, confidence=conf, + explanation=explanation, processing_time_ms=100.0 + ) + + +def test_fuser_imports(): + from src.fusion.fuser import Fuser + assert Fuser is not None + + +def test_fuser_image_returns_detection_response(engine_result_fake): + from src.fusion.fuser import Fuser + from src.engines.coherence.detector import CoherenceDetector + from src.engines.sstgnn.detector import SSTGNNDetector + fuser = Fuser() + results = [engine_result_fake, CoherenceDetector.image_stub(), SSTGNNDetector.image_stub()] + response = fuser.fuse(results, media_type="image", total_ms=100.0) + from src.types import DetectionResponse + assert isinstance(response, DetectionResponse) + assert response.verdict in ("FAKE", "REAL") + assert 0.0 <= response.confidence <= 1.0 + assert len(response.engine_breakdown) == 3 + + +def test_fuser_high_fake_confidence_returns_fake(): + from src.fusion.fuser import Fuser + fuser = Fuser() + results = [ + _make_result("fingerprint", "FAKE", 0.95), + _make_result("coherence", "FAKE", 0.88), + _make_result("sstgnn", "FAKE", 0.82), + ] + resp = fuser.fuse(results, media_type="video", total_ms=500.0) + assert resp.verdict == "FAKE" + assert resp.confidence > 0.5 + + +def test_fuser_high_real_confidence_returns_real(): + from src.fusion.fuser import Fuser + fuser = Fuser() + results = [ + _make_result("fingerprint", "REAL", 0.93), + _make_result("coherence", "REAL", 0.88), + _make_result("sstgnn", "REAL", 0.85), + ] + resp = fuser.fuse(results, media_type="video", total_ms=500.0) + assert resp.verdict == "REAL" + + +def test_fuser_empty_returns_default(): + from src.fusion.fuser import Fuser + fuser = Fuser() + resp = fuser.fuse([], media_type="image", total_ms=0.0) + assert resp.verdict in ("FAKE", "REAL") + + +def test_fuser_real_verdict_sets_generator_real(): + from src.fusion.fuser import Fuser + fuser = Fuser() + results = [_make_result("fingerprint", "REAL", 0.95)] + resp = fuser.fuse(results, media_type="image", total_ms=100.0) + if resp.verdict == "REAL": + assert resp.attributed_generator == GeneratorLabel.real + + +def test_types_generator_label_count(): + assert len(GeneratorLabel) == 8 + + +def test_types_generator_index_mapping(): + from src.types import GENERATOR_INDEX_TO_LABEL + assert GENERATOR_INDEX_TO_LABEL[0] == GeneratorLabel.real + assert GENERATOR_INDEX_TO_LABEL[1] == GeneratorLabel.unknown_gan + assert GENERATOR_INDEX_TO_LABEL[2] == GeneratorLabel.stable_diffusion + assert GENERATOR_INDEX_TO_LABEL[7] == GeneratorLabel.imagen + + +def test_engine_result_confidence_validator(): + with pytest.raises(Exception): + EngineResult( + engine="test", verdict="FAKE", confidence=1.5, + explanation="x", processing_time_ms=1.0 + ) + + +def test_engine_result_confidence_rounds(): + r = EngineResult( + engine="test", verdict="FAKE", confidence=0.923456789, + explanation="x", processing_time_ms=1.0 + ) + assert len(str(r.confidence).split(".")[-1]) <= 4 diff --git a/tests/training/__init__.py b/tests/training/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/training/test_datasets.py b/tests/training/test_datasets.py new file mode 100644 index 0000000000000000000000000000000000000000..fafe323edeaaf17620a1fe70137dd12706931c99 --- /dev/null +++ b/tests/training/test_datasets.py @@ -0,0 +1,108 @@ +"""tests/training/test_datasets.py""" +from __future__ import annotations + +import shutil +import uuid +from pathlib import Path + +import numpy as np +from PIL import Image + + +def _make_tiny_fingerprint_dataset(root: Path, n_per_class: int = 10) -> None: + """Create a minimal fake fingerprint dataset for testing.""" + for split in ["train", "val", "test"]: + for label in ["real", "fake"]: + d = root / split / label + d.mkdir(parents=True) + for i in range(n_per_class): + arr = (np.random.rand(64, 64, 3) * 255).astype(np.uint8) + Image.fromarray(arr).save(d / f"img_{i:04d}.jpg") + + +def _new_workspace_tmp(prefix: str) -> Path: + root = Path("tests") / ".tmp" / f"{prefix}_{uuid.uuid4().hex}" + root.mkdir(parents=True, exist_ok=True) + return root + + +def test_training_config_num_generator_classes(): + import sys + sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + from src.training.config import NUM_GENERATOR_CLASSES, GENERATOR_CLASSES + assert NUM_GENERATOR_CLASSES == 8 + assert len(GENERATOR_CLASSES) == 8 + assert GENERATOR_CLASSES[0] == "real" + assert GENERATOR_CLASSES[7] == "imagen" + + +def test_training_config_dataclass_defaults(): + from src.training.config import TrainingConfig + cfg = TrainingConfig() + assert cfg.num_generator_classes == 8 + assert cfg.num_binary_classes == 2 + assert cfg.seed == 42 + assert cfg.amp is True + + +def test_build_dataset_runs(): + """Smoke test: build_dataset.py should run without crashing on tiny data.""" + import sys + sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + + tmp_path = _new_workspace_tmp("build_dataset") + try: + # Create tiny fake source datasets + faces_dir = tmp_path / "faces_140k" + (faces_dir / "real").mkdir(parents=True) + (faces_dir / "fake").mkdir(parents=True) + for i in range(20): + for label in ["real", "fake"]: + arr = (np.random.rand(64, 64, 3) * 255).astype(np.uint8) + Image.fromarray(arr).save(faces_dir / label / f"img_{i}.jpg") + + output_dir = tmp_path / "processed" + + from training.phase1_fingerprint.build_dataset import build + import argparse + + args = argparse.Namespace( + faces_140k=str(faces_dir), + ai_vs_real=None, + deepfake_real=None, + deepfake_faces=None, + celebdf=None, + output_dir=str(output_dir), + seed=42, + ) + build(args) + + # Check output structure + assert (output_dir / "generator_labels.csv").exists() + for split in ["train", "val", "test"]: + for label in ["real", "fake"]: + assert (output_dir / split / label).exists() + finally: + shutil.rmtree(tmp_path, ignore_errors=True) + + +def test_fingerprint_dataset_class(): + """FingerprintDataset should load images correctly.""" + import sys + sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + tmp_path = _new_workspace_tmp("fingerprint_dataset") + try: + _make_tiny_fingerprint_dataset(tmp_path) + + from training.phase1_fingerprint.train import FingerprintDataset, get_val_transform + + ds = FingerprintDataset(tmp_path, "train", get_val_transform()) + + assert len(ds) == 20 # 10 real + 10 fake + sample = ds[0] + assert "image" in sample + assert "label" in sample + assert sample["image"].shape == (3, 224, 224) + assert sample["label"] in (0, 1) + finally: + shutil.rmtree(tmp_path, ignore_errors=True) diff --git a/tests/training/test_metrics.py b/tests/training/test_metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..5e1419469c89a5a8f70f2941d426edf997915d6a --- /dev/null +++ b/tests/training/test_metrics.py @@ -0,0 +1,66 @@ +"""tests/training/test_metrics.py""" +from __future__ import annotations + +import numpy as np +import pytest + + +def test_compute_auc_perfect(): + from src.training.metrics import compute_auc + y_true = [0, 0, 1, 1] + y_scores = [0.1, 0.2, 0.8, 0.9] + assert compute_auc(y_true, y_scores) == 1.0 + + +def test_compute_auc_random(): + from src.training.metrics import compute_auc + y_true = [0, 1, 0, 1] + y_scores = [0.4, 0.6, 0.5, 0.5] + auc = compute_auc(y_true, y_scores) + assert 0.0 <= auc <= 1.0 + + +def test_compute_auc_single_class_returns_half(): + from src.training.metrics import compute_auc + # All same class — undefined AUC, should return 0.5 + assert compute_auc([1, 1, 1], [0.9, 0.8, 0.7]) == 0.5 + + +def test_compute_eer_range(): + from src.training.metrics import compute_eer + y_true = [0, 0, 1, 1] + y_scores = [0.1, 0.3, 0.7, 0.9] + eer = compute_eer(y_true, y_scores) + assert 0.0 <= eer <= 1.0 + + +def test_optimal_threshold_returns_float(): + from src.training.metrics import optimal_threshold + y_true = [0, 0, 1, 1, 1] + y_scores = [0.1, 0.2, 0.6, 0.8, 0.9] + t = optimal_threshold(y_true, y_scores) + assert 0.0 <= t <= 1.0 + + +def test_compute_all_keys(): + from src.training.metrics import compute_all + y_true = [0, 0, 1, 1] + y_scores = [0.1, 0.2, 0.8, 0.9] + metrics = compute_all(y_true, y_scores, threshold=0.5) + for key in ["auc", "auc_pr", "eer", "f1", "accuracy", "precision", "recall", "threshold"]: + assert key in metrics, f"Missing key: {key}" + + +def test_training_config_consistency(): + """Generator class count must be 8 and consistent across config + types.""" + from src.training.config import NUM_GENERATOR_CLASSES, GENERATOR_CLASSES + from src.types import GeneratorLabel, GENERATOR_INDEX_TO_LABEL + + assert NUM_GENERATOR_CLASSES == 8 + assert len(GENERATOR_CLASSES) == 8 + assert len(GeneratorLabel) == 8 + assert len(GENERATOR_INDEX_TO_LABEL) == 8 + + # All class names must map to a valid GeneratorLabel + for name in GENERATOR_CLASSES: + assert GeneratorLabel(name) is not None diff --git a/training/download_datasets.sh b/training/download_datasets.sh new file mode 100644 index 0000000000000000000000000000000000000000..483966cab1030a0c235652b5144c669199c92e0d --- /dev/null +++ b/training/download_datasets.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# training/download_datasets.sh +# +# Downloads all training datasets via Kaggle CLI. +# +# Prerequisites: +# 1. kaggle.json in ~/.kaggle/ (chmod 600) +# 2. DFDC competition acceptance at: +# https://www.kaggle.com/competitions/deepfake-detection-challenge +# +# Run: bash training/download_datasets.sh + +set -e + +echo "=== Downloading GenAI-DeepDetect training datasets ===" + +mkdir -p data/raw/ff++ +mkdir -p data/raw/140k_faces +mkdir -p data/raw/dfdc +mkdir -p data/raw/celebdf +mkdir -p data/raw/deepfake_faces +mkdir -p data/raw/deepfake_real_images +mkdir -p data/raw/ai_vs_real + +# 1. FaceForensics++ (compressed version) +echo "[1/7] Downloading FaceForensics++ ..." +kaggle datasets download -d xhlulu/faceforensics-in-compressed-videos \ + -p data/raw/ff++ --unzip +echo "[1/7] FaceForensics++ done" + +# 2. 140k Real vs Fake Faces — primary dataset for Fingerprint Engine +echo "[2/7] Downloading 140k Real vs Fake Faces ..." +kaggle datasets download -d xhlulu/140k-real-and-fake-faces \ + -p data/raw/140k_faces --unzip +echo "[2/7] 140k Faces done" + +# 3. DFDC — REQUIRES competition acceptance first! +# Go to: https://www.kaggle.com/competitions/deepfake-detection-challenge +# Click "Join Competition" and accept the rules. Then this works: +echo "[3/7] Downloading DFDC (requires prior competition acceptance) ..." +kaggle competitions download -c deepfake-detection-challenge \ + -p data/raw/dfdc --unzip +echo "[3/7] DFDC done" + +# 4. Celeb-DF v2 +echo "[4/7] Downloading Celeb-DF v2 ..." +kaggle datasets download -d reubensinclair/celeb-df \ + -p data/raw/celebdf --unzip +echo "[4/7] Celeb-DF done" + +# 5. Deepfake Faces (extra negative samples) +echo "[5/7] Downloading Deepfake Faces ..." +kaggle datasets download -d dagnelies/deepfake-faces \ + -p data/raw/deepfake_faces --unzip +echo "[5/7] Deepfake Faces done" + +# 6. Deepfake and Real Images +echo "[6/7] Downloading Deepfake and Real Images ..." +kaggle datasets download -d manjilkarki/deepfake-and-real-images \ + -p data/raw/deepfake_real_images --unzip +echo "[6/7] Deepfake Real Images done" + +# 7. AI Generated vs Real +echo "[7/7] Downloading AI Generated vs Real ..." +kaggle datasets download -d philosopher0/ai-generated-vs-real-images-datasaet \ + -p data/raw/ai_vs_real --unzip +echo "[7/7] AI vs Real done" + +echo "" +echo "=== All datasets downloaded ===" +du -sh data/raw/*/ diff --git a/training/phase1_fingerprint/__init__.py b/training/phase1_fingerprint/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/training/phase1_fingerprint/augmentation.py b/training/phase1_fingerprint/augmentation.py new file mode 100644 index 0000000000000000000000000000000000000000..d884eeeba81bf4c335c044c3d9ac7d47f7eefd35 --- /dev/null +++ b/training/phase1_fingerprint/augmentation.py @@ -0,0 +1,78 @@ +""" +training/phase1_fingerprint/augmentation.py + +Albumentations augmentation pipelines for the Fingerprint engine. +ImageCompression augmentation is critical — without it, models learn +JPEG artifacts instead of synthetic fingerprints. +""" +from __future__ import annotations + +import numpy as np + +try: + import albumentations as A + from albumentations.pytorch import ToTensorV2 + ALBUMENTATIONS_AVAILABLE = True +except ImportError: + ALBUMENTATIONS_AVAILABLE = False + + +MEAN = [0.485, 0.456, 0.406] +STD = [0.229, 0.224, 0.225] + + +def get_train_transform(size: int = 224): + """Strong augmentation for training — simulates social-media compression.""" + if not ALBUMENTATIONS_AVAILABLE: + from torchvision import transforms + return transforms.Compose([ + transforms.RandomResizedCrop(size, scale=(0.8, 1.0)), + transforms.RandomHorizontalFlip(), + transforms.ColorJitter(0.2, 0.2, 0.2, 0.1), + transforms.ToTensor(), + transforms.Normalize(MEAN, STD), + ]) + + return A.Compose([ + A.RandomResizedCrop(size, size, scale=(0.8, 1.0)), + A.HorizontalFlip(p=0.5), + A.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1, p=0.5), + A.GaussNoise(var_limit=(10.0, 50.0), p=0.3), + # Simulate JPEG re-compression — critical for real-world robustness + A.ImageCompression(quality_lower=60, quality_upper=95, p=0.4), + A.Normalize(mean=MEAN, std=STD), + ToTensorV2(), + ]) + + +def get_val_transform(size: int = 224): + """Minimal deterministic transform for validation and inference.""" + if not ALBUMENTATIONS_AVAILABLE: + from torchvision import transforms + return transforms.Compose([ + transforms.Resize(int(size * 256 / 224)), + transforms.CenterCrop(size), + transforms.ToTensor(), + transforms.Normalize(MEAN, STD), + ]) + + return A.Compose([ + A.Resize(int(size * 256 / 224), int(size * 256 / 224)), + A.CenterCrop(size, size), + A.Normalize(mean=MEAN, std=STD), + ToTensorV2(), + ]) + + +def apply_transform(transform, image: np.ndarray) -> "torch.Tensor": + """ + Apply either a torchvision or albumentations transform to a numpy image. + image: (H, W, 3) uint8 + """ + if ALBUMENTATIONS_AVAILABLE and hasattr(transform, 'transforms'): + result = transform(image=image) + return result["image"] + else: + from PIL import Image + pil_img = Image.fromarray(image) + return transform(pil_img) diff --git a/training/phase1_fingerprint/build_dataset.py b/training/phase1_fingerprint/build_dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..f22b0f4b13912bd3d33178dbd6f5250fbff6f07e --- /dev/null +++ b/training/phase1_fingerprint/build_dataset.py @@ -0,0 +1,309 @@ +""" +training/phase1_fingerprint/build_dataset.py + +Builds a unified image dataset for the Fingerprint engine from multiple sources. + +Output structure: + data/processed/fingerprint/ + train/real/ + train/fake/ + val/real/ + val/fake/ + test/real/ + test/fake/ + generator_labels.csv ← filepath, is_fake, generator (0-7 int) + +Run: + python training/phase1_fingerprint/build_dataset.py --help +""" + +from __future__ import annotations + +import argparse +import csv +import hashlib +import logging +import random +import shutil +import sys +from collections import Counter, defaultdict +from pathlib import Path + +import numpy as np + +# ── Logging ────────────────────────────────────────────────────────────────── + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s %(message)s", + datefmt="%H:%M:%S", +) +log = logging.getLogger(__name__) + + +# ── Generator label mapping ─────────────────────────────────────────────────── + +GENERATOR_MAP: dict[str, int] = { + "real": 0, + "unknown_gan": 1, + "stable_diffusion": 2, + "midjourney": 3, + "dall_e": 4, + "flux": 5, + "firefly": 6, + "imagen": 7, +} + +IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp"} + + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +def md5(path: Path) -> str: + h = hashlib.md5() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + h.update(chunk) + return h.hexdigest() + + +def is_image(path: Path) -> bool: + return path.suffix.lower() in IMAGE_EXTS + + +def collect_images(root: Path) -> list[Path]: + return [p for p in root.rglob("*") if p.is_file() and is_image(p)] + + +# ── Dataset loaders ─────────────────────────────────────────────────────────── + +def load_140k_faces(root: Path) -> list[tuple[Path, int, int]]: + """Returns [(path, is_fake, generator_idx)]""" + records = [] + for split_dir in ["train", "valid"]: + for label in ["real", "fake"]: + d = root / split_dir / label + if not d.exists(): + # also try without nested split + d = root / label + if not d.exists(): + continue + is_fake = 1 if label == "fake" else 0 + gen = GENERATOR_MAP["unknown_gan"] if is_fake else GENERATOR_MAP["real"] + for p in collect_images(d): + records.append((p, is_fake, gen)) + log.info(f"140k_faces: {len(records)} images") + return records + + +def load_ai_vs_real(root: Path) -> list[tuple[Path, int, int]]: + """Subfolder name = generator name (or 'real').""" + records = [] + for subfolder in sorted(root.iterdir()): + if not subfolder.is_dir(): + continue + name = subfolder.name.lower().replace("-", "_").replace(" ", "_") + if "real" in name: + gen_idx = GENERATOR_MAP["real"] + is_fake = 0 + else: + # Map folder names to known generators + gen_idx = GENERATOR_MAP.get(name, GENERATOR_MAP["unknown_gan"]) + is_fake = 1 + for p in collect_images(subfolder): + records.append((p, is_fake, gen_idx)) + log.info(f"ai_vs_real: {len(records)} images") + return records + + +def load_deepfake_real_images(root: Path) -> list[tuple[Path, int, int]]: + """Folder structure: real/ and fake/ at root or one level down.""" + records = [] + for label in ["real", "fake"]: + # try root/label and root/*/label + candidates = list(root.glob(f"{label}")) + list(root.glob(f"*/{label}")) + for d in candidates: + if not d.is_dir(): + continue + is_fake = 1 if label == "fake" else 0 + gen = GENERATOR_MAP["unknown_gan"] if is_fake else GENERATOR_MAP["real"] + for p in collect_images(d): + records.append((p, is_fake, gen)) + log.info(f"deepfake_real_images: {len(records)} images") + return records + + +def load_deepfake_faces(root: Path) -> list[tuple[Path, int, int]]: + """All fake, generator = unknown_gan.""" + records = [] + for p in collect_images(root): + records.append((p, 1, GENERATOR_MAP["unknown_gan"])) + log.info(f"deepfake_faces: {len(records)} images (all fake)") + return records + + +def load_celebdf_images(root: Path) -> list[tuple[Path, int, int]]: + """Treat all images in 'Celeb-real'/'YouTube-real' as real, rest as fake.""" + records = [] + for p in collect_images(root): + parts = [part.lower() for part in p.parts] + is_real = any("real" in part for part in parts) + is_fake = 0 if is_real else 1 + gen = GENERATOR_MAP["real"] if is_real else GENERATOR_MAP["unknown_gan"] + records.append((p, is_fake, gen)) + log.info(f"celebdf: {len(records)} images") + return records + + +# ── Splitting + copying ─────────────────────────────────────────────────────── + +def split_records( + records: list[tuple[Path, int, int]], + seed: int = 42, + train_ratio: float = 0.80, + val_ratio: float = 0.10, +) -> dict[str, list[tuple[Path, int, int]]]: + rng = random.Random(seed) + shuffled = list(records) + rng.shuffle(shuffled) + n = len(shuffled) + n_train = int(n * train_ratio) + n_val = int(n * val_ratio) + return { + "train": shuffled[:n_train], + "val": shuffled[n_train:n_train + n_val], + "test": shuffled[n_train + n_val:], + } + + +def copy_to_split( + records: list[tuple[Path, int, int]], + split: str, + output_dir: Path, + seen_hashes: set[str], +) -> list[dict]: + """Copy images to output_dir/{split}/{real|fake}/ and return CSV rows.""" + rows = [] + dupes = 0 + for src, is_fake, gen in records: + h = md5(src) + if h in seen_hashes: + dupes += 1 + continue + seen_hashes.add(h) + + label_dir = output_dir / split / ("fake" if is_fake else "real") + label_dir.mkdir(parents=True, exist_ok=True) + + # Unique filename: hash prefix + original name + dst = label_dir / f"{h[:8]}_{src.name}" + if not dst.exists(): + shutil.copy2(src, dst) + + rows.append({ + "filepath": str(dst.relative_to(output_dir)), + "is_fake": is_fake, + "generator": gen, + }) + + if dupes: + log.warning(f" {split}: skipped {dupes} duplicate files") + return rows + + +# ── Main ───────────────────────────────────────────────────────────────────── + +def build(args: argparse.Namespace) -> None: + rng_seed = args.seed + np.random.seed(rng_seed) + random.seed(rng_seed) + + output_dir = Path(args.output_dir) + + # ── Load all sources ────────────────────────────────────────────────────── + all_records: list[tuple[Path, int, int]] = [] + + if args.faces_140k and Path(args.faces_140k).exists(): + all_records += load_140k_faces(Path(args.faces_140k)) + if args.ai_vs_real and Path(args.ai_vs_real).exists(): + all_records += load_ai_vs_real(Path(args.ai_vs_real)) + if args.deepfake_real and Path(args.deepfake_real).exists(): + all_records += load_deepfake_real_images(Path(args.deepfake_real)) + if args.deepfake_faces and Path(args.deepfake_faces).exists(): + all_records += load_deepfake_faces(Path(args.deepfake_faces)) + if args.celebdf and Path(args.celebdf).exists(): + all_records += load_celebdf_images(Path(args.celebdf)) + + if not all_records: + log.error("No images loaded — check dataset paths with --help") + sys.exit(1) + + log.info(f"Total records loaded: {len(all_records)}") + + # ── Split ───────────────────────────────────────────────────────────────── + splits = split_records(all_records, seed=rng_seed) + for s, recs in splits.items(): + log.info(f" {s}: {len(recs)} images") + + # ── Assertions ──────────────────────────────────────────────────────────── + MIN_PER_SPLIT = int(getattr(args, "min_per_split", 1)) + for s, recs in splits.items(): + assert len(recs) >= MIN_PER_SPLIT, \ + f"Split '{s}' only has {len(recs)} images, need >= {MIN_PER_SPLIT}" + + # ── Copy + write CSV ────────────────────────────────────────────────────── + seen_hashes: set[str] = set() + all_csv_rows: list[dict] = [] + + for split, records in splits.items(): + log.info(f"Copying {split} split ...") + (output_dir / split / "real").mkdir(parents=True, exist_ok=True) + (output_dir / split / "fake").mkdir(parents=True, exist_ok=True) + rows = copy_to_split(records, split, output_dir, seen_hashes) + all_csv_rows += rows + log.info(f" {split}: {len(rows)} files written") + + csv_path = output_dir / "generator_labels.csv" + with open(csv_path, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=["filepath", "is_fake", "generator"]) + writer.writeheader() + writer.writerows(all_csv_rows) + + log.info(f"generator_labels.csv written: {len(all_csv_rows)} rows") + + # ── Summary ─────────────────────────────────────────────────────────────── + total = len(all_csv_rows) + n_fake = sum(r["is_fake"] for r in all_csv_rows) + n_real = total - n_fake + gen_dist = Counter(r["generator"] for r in all_csv_rows) + + print("\n=== Dataset summary ===") + print(f"Total images : {total:,}") + print(f"Real : {n_real:,} ({100*n_real/total:.1f}%)") + print(f"Fake : {n_fake:,} ({100*n_fake/total:.1f}%)") + print("\nGenerator distribution:") + gen_names = ["real", "unknown_gan", "stable_diffusion", "midjourney", + "dall_e", "flux", "firefly", "imagen"] + for idx, name in enumerate(gen_names): + count = gen_dist.get(idx, 0) + if count: + print(f" {idx} {name:20s}: {count:,}") + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description="Build unified fingerprint dataset") + p.add_argument("--faces_140k", default=None, help="Path to 140k-real-and-fake-faces") + p.add_argument("--ai_vs_real", default=None, help="Path to ai-generated-vs-real-images") + p.add_argument("--deepfake_real", default=None, help="Path to deepfake-and-real-images") + p.add_argument("--deepfake_faces",default=None, help="Path to deepfake-faces") + p.add_argument("--celebdf", default=None, help="Path to celeb-df dataset") + p.add_argument("--output_dir", default="data/processed/fingerprint", + help="Where to write the processed dataset") + p.add_argument("--seed", type=int, default=42) + p.add_argument("--min_per_split", type=int, default=1000, + help="Minimum items required in each split") + return p.parse_args() + + +if __name__ == "__main__": + build(parse_args()) diff --git a/training/phase1_fingerprint/dataset.py b/training/phase1_fingerprint/dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..f13a5dc249a75d7b2b588a2e7000684c86b1a610 --- /dev/null +++ b/training/phase1_fingerprint/dataset.py @@ -0,0 +1,93 @@ +""" +training/phase1_fingerprint/dataset.py + +PyTorch Dataset for the Fingerprint engine. +Reads the directory structure produced by build_dataset.py. +""" +from __future__ import annotations + +import csv +from pathlib import Path +from typing import Optional + +import torch +from torch.utils.data import Dataset +from PIL import Image + +IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp"} + + +class FingerprintDataset(Dataset): + """ + Loads images from: + root_dir/{split}/real/*.jpg + root_dir/{split}/fake/*.jpg + + And generator labels from root_dir/generator_labels.csv. + """ + + def __init__( + self, + root_dir: Path, + split: str, # 'train' | 'val' | 'test' + transform = None, + ) -> None: + self.root_dir = Path(root_dir) + self.split = split + self.transform = transform + self.samples: list[dict] = [] + + # Load generator label CSV + gen_map: dict[str, int] = {} + csv_path = self.root_dir / "generator_labels.csv" + if csv_path.exists(): + with open(csv_path) as f: + for row in csv.DictReader(f): + gen_map[row["filepath"]] = int(row["generator"]) + + # Walk split dirs + split_dir = self.root_dir / split + for label_str, is_fake in [("real", 0), ("fake", 1)]: + d = split_dir / label_str + if not d.exists(): + continue + for p in sorted(d.iterdir()): + if p.suffix.lower() not in IMAGE_EXTS: + continue + rel = str(p.relative_to(self.root_dir)) + gen_idx = gen_map.get(rel, 0 if is_fake == 0 else 1) + self.samples.append({ + "path": p, + "label": is_fake, + "generator": gen_idx, + }) + + def __len__(self) -> int: + return len(self.samples) + + def __getitem__(self, idx: int) -> dict: + s = self.samples[idx] + img = Image.open(s["path"]).convert("RGB") + if self.transform: + import numpy as np + from training.phase1_fingerprint.augmentation import apply_transform + img_np = apply_transform(self.transform, img if isinstance(img, type(img)) else img) + if not isinstance(img_np, torch.Tensor): + from torchvision import transforms as T + img_np = T.ToTensor()(img) + else: + import torchvision.transforms.functional as TF + img_np = TF.to_tensor(img) + + return { + "image": img_np, + "label": s["label"], + "generator": s["generator"], + "filepath": str(s["path"]), + } + + def get_class_weights(self) -> torch.Tensor: + labels = [s["label"] for s in self.samples] + n_real = max(labels.count(0), 1) + n_fake = max(labels.count(1), 1) + return torch.tensor([1.0 / n_real, 1.0 / n_fake], dtype=torch.float32) diff --git a/training/phase1_fingerprint/integrate.py b/training/phase1_fingerprint/integrate.py new file mode 100644 index 0000000000000000000000000000000000000000..41b03158a190560035aafd56b962f3262b03b8cc --- /dev/null +++ b/training/phase1_fingerprint/integrate.py @@ -0,0 +1,95 @@ +""" +training/phase1_fingerprint/integrate.py + +Run after Kaggle training completes. +Validates the checkpoint, copies it to production, and prints integration steps. + +Usage: + python training/phase1_fingerprint/integrate.py \ + --ckpt models/checkpoints/fingerprint/best.pt +""" +from __future__ import annotations + +import argparse +import json +import shutil +from datetime import date +from pathlib import Path + +import torch + + +def main(args: argparse.Namespace) -> None: + ckpt_path = Path(args.ckpt) + assert ckpt_path.exists(), f"Checkpoint not found: {ckpt_path}" + + # Load and inspect + ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=True) + val_auc = ckpt.get("val_auc", "unknown") + epoch = ckpt.get("epoch", "unknown") + config = ckpt.get("config", {}) + + print(f"\n=== Fingerprint checkpoint ===") + print(f" Epoch: {epoch}") + print(f" Val AUC: {val_auc}") + print(f" Config: {json.dumps(config, indent=4)}") + + # Validate model loads correctly + import sys + sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + from training.phase1_fingerprint.model import build_fingerprint_model + + model = build_fingerprint_model(pretrained=False) + model.load_state_dict(ckpt["model_state"]) + model.eval() + print(f"\n Model loaded successfully.") + + # Quick forward pass + import torch + dummy = torch.randn(1, 3, 224, 224) + with torch.no_grad(): + out = model(dummy) + assert out["binary_logits"].shape == (1, 2) + assert out["generator_logits"].shape == (1, 8) + print(f" Forward pass OK — binary (1,2), generator (1,8).") + + # Copy to production + prod_dir = Path("models/checkpoints/fingerprint") + prod_dir.mkdir(parents=True, exist_ok=True) + prod_path = prod_dir / "production.pt" + shutil.copy2(ckpt_path, prod_path) + print(f"\n Copied to: {prod_path}") + + # Write README + readme_path = prod_dir / "README.txt" + with open(readme_path, "w") as f: + f.write(f"GenAI-DeepDetect Fingerprint Engine Checkpoint\n") + f.write(f"Training date: {date.today()}\n") + f.write(f"Val AUC: {val_auc}\n") + f.write(f"Epoch: {epoch}\n") + f.write(f"Config: {json.dumps(config, indent=2)}\n") + print(f" README written: {readme_path}") + + print(f""" +=== Integration steps === + +1. The detector already loads from this path automatically: + models/checkpoints/fingerprint/best.pt + models/checkpoints/fingerprint/production.pt + +2. Verify the server picks it up: + python -c "from src.engines.fingerprint.detector import FingerprintDetector; d = FingerprintDetector(); print('Loaded')" + +3. Run integration tests: + python training/phase6_export/integration_test.py +""") + + +def parse_args(): + p = argparse.ArgumentParser() + p.add_argument("--ckpt", default="models/checkpoints/fingerprint/best.pt") + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/training/phase1_fingerprint/model.py b/training/phase1_fingerprint/model.py new file mode 100644 index 0000000000000000000000000000000000000000..98f622e305844e04f2c214bd6b174e20e5928734 --- /dev/null +++ b/training/phase1_fingerprint/model.py @@ -0,0 +1,134 @@ +""" +training/phase1_fingerprint/model.py + +ViT-B/16 fine-tuned for deepfake detection with two heads: + 1. binary_head: real vs fake (2 classes) + 2. generator_head: generator attribution (8 classes) + +Architecture basis: Causal Fingerprints (arXiv:2509.15406) +""" + +from __future__ import annotations + +import torch +import torch.nn as nn +import timm + +NUM_GENERATOR_CLASSES = 8 + + +class FingerprintModel(nn.Module): + """ + ViT-B/16 backbone with dual classification heads. + + forward(x) returns: + { + 'binary_logits': Tensor (B, 2) + 'generator_logits': Tensor (B, 8) + } + """ + + def __init__( + self, + model_name: str = "vit_base_patch16_224", + pretrained: bool = True, + num_binary_classes: int = 2, + num_generator_classes: int = NUM_GENERATOR_CLASSES, + dropout: float = 0.1, + ) -> None: + super().__init__() + + # Load backbone — drop default head + self.backbone = timm.create_model( + model_name, + pretrained=pretrained, + num_classes=0, # removes the head + global_pool="token", # returns [CLS] token, shape (B, 768) + ) + embed_dim = self.backbone.num_features # 768 for ViT-B + + self.drop = nn.Dropout(dropout) + + # Parallel heads on [CLS] embedding + self.binary_head = nn.Sequential( + nn.Linear(embed_dim, embed_dim // 2), + nn.GELU(), + nn.Dropout(dropout), + nn.Linear(embed_dim // 2, num_binary_classes), + ) + self.generator_head = nn.Sequential( + nn.Linear(embed_dim, embed_dim // 2), + nn.GELU(), + nn.Dropout(dropout), + nn.Linear(embed_dim // 2, num_generator_classes), + ) + + def forward(self, x: torch.Tensor) -> dict[str, torch.Tensor]: + features = self.backbone(x) # (B, 768) + features = self.drop(features) + return { + "binary_logits": self.binary_head(features), # (B, 2) + "generator_logits": self.generator_head(features), # (B, 8) + } + + @property + def config(self) -> dict: + return { + "num_binary_classes": 2, + "num_generator_classes": NUM_GENERATOR_CLASSES, + } + + +class FingerprintLoss(nn.Module): + """ + Combined binary + generator attribution loss. + + binary_weight: weight for real/fake classification (primary) + generator_weight: weight for generator attribution (secondary, default 0.3) + """ + + def __init__( + self, + class_weights: torch.Tensor | None = None, + generator_weight: float = 0.3, + ) -> None: + super().__init__() + self.generator_weight = generator_weight + + self.binary_loss = nn.CrossEntropyLoss( + weight=class_weights, + label_smoothing=0.05, + ) + # ignore_index=-1 for samples where generator is unknown + self.generator_loss = nn.CrossEntropyLoss( + ignore_index=-1, + label_smoothing=0.05, + ) + + def forward( + self, + outputs: dict[str, torch.Tensor], + binary_labels: torch.Tensor, + generator_labels: torch.Tensor, + ) -> dict[str, torch.Tensor]: + b_loss = self.binary_loss(outputs["binary_logits"], binary_labels) + g_loss = self.generator_loss(outputs["generator_logits"], generator_labels) + + total = b_loss + self.generator_weight * g_loss + + return { + "total": total, + "binary": b_loss, + "generator": g_loss, + } + + +def build_fingerprint_model( + pretrained: bool = True, + num_generator_classes: int = NUM_GENERATOR_CLASSES, +) -> FingerprintModel: + """Convenience constructor used by both training and detector.""" + return FingerprintModel( + pretrained=pretrained, + num_generator_classes=num_generator_classes, + ) diff --git a/training/phase1_fingerprint/train.py b/training/phase1_fingerprint/train.py new file mode 100644 index 0000000000000000000000000000000000000000..8d240049cd9fbb9ac726c7438e347b242829f2f4 --- /dev/null +++ b/training/phase1_fingerprint/train.py @@ -0,0 +1,397 @@ +""" +training/phase1_fingerprint/train.py + +Full training script for the Fingerprint engine. + +Kaggle usage: + !python scripts/train_image_baseline.py --data_dir ... --output_dir ... + +Direct usage: + python training/phase1_fingerprint/train.py \ + --data_dir data/processed/fingerprint \ + --output_dir models/checkpoints/fingerprint \ + --epochs 30 --batch_size 64 --lr 2e-5 --amp +""" + +from __future__ import annotations + +import argparse +import csv +import logging +import math +import random +import time +from datetime import datetime +from pathlib import Path + +import numpy as np +import torch +import torch.nn as nn +from torch.cuda.amp import GradScaler, autocast +from torch.utils.data import DataLoader, Dataset, WeightedRandomSampler +from torchvision import transforms +from PIL import Image +from sklearn.metrics import ( + accuracy_score, roc_auc_score, f1_score, + precision_score, recall_score, confusion_matrix, + classification_report, +) +from tqdm import tqdm + +# Local imports (run from repo root) +import sys +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +# ── Logging ─────────────────────────────────────────────────────────────────── + +def setup_logging(output_dir: Path) -> None: + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + log_dir = Path("training/logs") + log_dir.mkdir(parents=True, exist_ok=True) + log_file = log_dir / f"phase1_fingerprint_{ts}.log" + + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s %(message)s", + handlers=[ + logging.StreamHandler(), + logging.FileHandler(log_file), + ], + ) + +log = logging.getLogger(__name__) + + +# ── Transforms ──────────────────────────────────────────────────────────────── + +MEAN = [0.485, 0.456, 0.406] +STD = [0.229, 0.224, 0.225] + +def get_train_transform(): + return transforms.Compose([ + transforms.RandomResizedCrop(224, scale=(0.8, 1.0)), + transforms.RandomHorizontalFlip(p=0.5), + transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1), + transforms.ToTensor(), + # Simulate JPEG compression artifacts + transforms.RandomApply([ + transforms.GaussianBlur(kernel_size=3, sigma=(0.1, 1.0)) + ], p=0.3), + transforms.Normalize(MEAN, STD), + ]) + +def get_val_transform(): + return transforms.Compose([ + transforms.Resize(256), + transforms.CenterCrop(224), + transforms.ToTensor(), + transforms.Normalize(MEAN, STD), + ]) + + +# ── Dataset ─────────────────────────────────────────────────────────────────── + +class FingerprintDataset(Dataset): + def __init__(self, root_dir: Path, split: str, transform=None): + self.root_dir = Path(root_dir) + self.split = split + self.transform = transform + self.samples: list[dict] = [] + + # Load generator labels CSV + csv_path = self.root_dir / "generator_labels.csv" + gen_map: dict[str, dict] = {} + if csv_path.exists(): + with open(csv_path) as f: + for row in csv.DictReader(f): + gen_map[row["filepath"]] = row + + # Walk split directories + split_dir = self.root_dir / split + for label_dir in ["real", "fake"]: + d = split_dir / label_dir + if not d.exists(): + continue + is_fake = 1 if label_dir == "fake" else 0 + for img_path in sorted(d.iterdir()): + if img_path.suffix.lower() not in {".jpg", ".jpeg", ".png", ".webp"}: + continue + rel = str(img_path.relative_to(self.root_dir)) + gen_idx = int(gen_map[rel]["generator"]) if rel in gen_map else (1 if is_fake else 0) + self.samples.append({ + "path": img_path, + "label": is_fake, + "generator": gen_idx, + }) + + log.info(f"FingerprintDataset [{split}]: {len(self.samples)} samples") + + def __len__(self) -> int: + return len(self.samples) + + def __getitem__(self, idx: int) -> dict: + s = self.samples[idx] + img = Image.open(s["path"]).convert("RGB") + if self.transform: + img = self.transform(img) + return { + "image": img, + "label": s["label"], + "generator": s["generator"], + "filepath": str(s["path"]), + } + + def get_class_weights(self) -> torch.Tensor: + labels = [s["label"] for s in self.samples] + counts = [labels.count(0), labels.count(1)] + weights = [1.0 / (c + 1e-6) for c in counts] + return torch.tensor(weights, dtype=torch.float32) + + +# ── LR schedule ─────────────────────────────────────────────────────────────── + +def get_lr_with_warmup(step: int, warmup_steps: int, total_steps: int, base_lr: float) -> float: + if step < warmup_steps: + return base_lr * step / warmup_steps + progress = (step - warmup_steps) / max(1, total_steps - warmup_steps) + return base_lr * 0.5 * (1.0 + math.cos(math.pi * progress)) + + +# ── Training ────────────────────────────────────────────────────────────────── + +def train_epoch( + model, loader, criterion, optimizer, scaler, device, args, epoch, total_steps +) -> dict: + model.train() + total_loss = binary_loss_sum = gen_loss_sum = 0.0 + all_preds, all_labels = [], [] + step_offset = epoch * len(loader) + + pbar = tqdm(loader, desc=f"Train E{epoch+1}", leave=False) + for i, batch in enumerate(pbar): + images = batch["image"].to(device) + labels = batch["label"].to(device) + gen_lbls = batch["generator"].to(device) + + global_step = step_offset + i + + # LR warmup + lr = get_lr_with_warmup( + global_step, args.warmup_steps, + args.epochs * len(loader), args.lr + ) + for g in optimizer.param_groups: + g["lr"] = lr + + with autocast(enabled=args.amp): + outputs = model(images) + losses = criterion(outputs, labels, gen_lbls) + + loss = losses["total"] / args.grad_accum + scaler.scale(loss).backward() + + if (i + 1) % args.grad_accum == 0: + scaler.unscale_(optimizer) + nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) + scaler.step(optimizer) + scaler.update() + optimizer.zero_grad() + + total_loss += losses["total"].item() + binary_loss_sum += losses["binary"].item() + gen_loss_sum += losses["generator"].item() + + preds = outputs["binary_logits"].argmax(dim=1).cpu().numpy() + all_preds.extend(preds) + all_labels.extend(labels.cpu().numpy()) + + if i % 50 == 0: + acc = accuracy_score(all_labels, all_preds) + pbar.set_postfix(loss=f"{losses['total'].item():.4f}", acc=f"{acc:.3f}", lr=f"{lr:.2e}") + + n = len(loader) + return { + "loss": total_loss / n, + "binary_loss": binary_loss_sum / n, + "gen_loss": gen_loss_sum / n, + "accuracy": accuracy_score(all_labels, all_preds), + } + + +@torch.no_grad() +def validate(model, loader, criterion, device, args) -> dict: + model.eval() + total_loss = 0.0 + all_preds, all_labels, all_probs = [], [], [] + + for batch in tqdm(loader, desc="Val", leave=False): + images = batch["image"].to(device) + labels = batch["label"].to(device) + gen_lbls = batch["generator"].to(device) + + with autocast(enabled=args.amp): + outputs = model(images) + losses = criterion(outputs, labels, gen_lbls) + + total_loss += losses["total"].item() + + probs = torch.softmax(outputs["binary_logits"], dim=1)[:, 1] + preds = outputs["binary_logits"].argmax(dim=1) + + all_probs.extend(probs.cpu().numpy()) + all_preds.extend(preds.cpu().numpy()) + all_labels.extend(labels.cpu().numpy()) + + auc = roc_auc_score(all_labels, all_probs) if len(set(all_labels)) > 1 else 0.5 + return { + "loss": total_loss / len(loader), + "auc": auc, + "accuracy": accuracy_score(all_labels, all_preds), + "f1": f1_score(all_labels, all_preds, zero_division=0), + "precision": precision_score(all_labels, all_preds, zero_division=0), + "recall": recall_score(all_labels, all_preds, zero_division=0), + } + + +def save_checkpoint(path: Path, model, optimizer, scaler, epoch, val_auc, args) -> None: + torch.save({ + "epoch": epoch, + "model_state": model.state_dict(), + "optimizer": optimizer.state_dict(), + "scaler": scaler.state_dict(), + "val_auc": val_auc, + "config": vars(args), + }, path) + + +def main(args: argparse.Namespace) -> None: + from training.phase1_fingerprint.model import FingerprintLoss, build_fingerprint_model + + setup_logging(Path(args.output_dir)) + torch.manual_seed(args.seed) + np.random.seed(args.seed) + random.seed(args.seed) + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + log.info(f"Device: {device}") + if device.type == "cuda": + log.info(f"GPU: {torch.cuda.get_device_name(0)}") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + # ── Datasets ────────────────────────────────────────────────────────────── + train_ds = FingerprintDataset(args.data_dir, "train", get_train_transform()) + val_ds = FingerprintDataset(args.data_dir, "val", get_val_transform()) + + # Weighted sampler for class balance + weights = [train_ds.get_class_weights()[s["label"]] for s in train_ds.samples] + sampler = WeightedRandomSampler(weights, num_samples=len(weights), replacement=True) + + train_loader = DataLoader( + train_ds, batch_size=args.batch_size, sampler=sampler, + num_workers=args.workers, pin_memory=True, + ) + val_loader = DataLoader( + val_ds, batch_size=args.batch_size * 2, shuffle=False, + num_workers=args.workers, pin_memory=True, + ) + + # ── Model ───────────────────────────────────────────────────────────────── + model = build_fingerprint_model(pretrained=True).to(device) + class_weights = train_ds.get_class_weights().to(device) + criterion = FingerprintLoss(class_weights=class_weights) + + optimizer = torch.optim.AdamW( + model.parameters(), lr=args.lr, weight_decay=args.weight_decay + ) + scaler = GradScaler(enabled=args.amp) + + # ── Resume ──────────────────────────────────────────────────────────────── + start_epoch = 0 + best_auc = 0.0 + no_improve = 0 + + if args.resume and Path(args.resume).exists(): + ckpt = torch.load(args.resume, map_location=device, weights_only=True) + model.load_state_dict(ckpt["model_state"]) + optimizer.load_state_dict(ckpt["optimizer"]) + scaler.load_state_dict(ckpt["scaler"]) + start_epoch = ckpt["epoch"] + 1 + best_auc = ckpt["val_auc"] + log.info(f"Resumed from {args.resume} at epoch {start_epoch}, best AUC={best_auc:.4f}") + + # ── Training loop ───────────────────────────────────────────────────────── + log.info(f"Training for {args.epochs} epochs") + for epoch in range(start_epoch, args.epochs): + t0 = time.time() + + train_metrics = train_epoch( + model, train_loader, criterion, optimizer, scaler, device, args, + epoch, args.epochs * len(train_loader) + ) + val_metrics = validate(model, val_loader, criterion, device, args) + + elapsed = time.time() - t0 + log.info( + f"Epoch {epoch+1:02d}/{args.epochs} | " + f"train_loss={train_metrics['loss']:.4f} | " + f"val_auc={val_metrics['auc']:.4f} | " + f"val_acc={val_metrics['accuracy']:.4f} | " + f"val_f1={val_metrics['f1']:.4f} | " + f"{elapsed:.0f}s" + ) + + # Save epoch checkpoint + save_checkpoint( + output_dir / f"epoch_{epoch+1:02d}.pt", + model, optimizer, scaler, epoch, val_metrics["auc"], args + ) + # Also save latest for --resume + save_checkpoint( + output_dir / "latest.pt", + model, optimizer, scaler, epoch, val_metrics["auc"], args + ) + + # Best checkpoint + if val_metrics["auc"] > best_auc: + best_auc = val_metrics["auc"] + no_improve = 0 + save_checkpoint( + output_dir / "best.pt", + model, optimizer, scaler, epoch, best_auc, args + ) + log.info(f" New best AUC: {best_auc:.4f} — checkpoint saved") + else: + no_improve += 1 + log.info(f" No improvement ({no_improve}/{args.patience})") + + if no_improve >= args.patience: + log.info(f"Early stopping triggered at epoch {epoch+1}") + break + + log.info(f"Training complete. Best val AUC: {best_auc:.4f}") + log.info(f"Best checkpoint: {output_dir / 'best.pt'}") + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description="Train Fingerprint engine (Phase 1)") + p.add_argument("--data_dir", required=True, help="Path to processed fingerprint dataset") + p.add_argument("--output_dir", required=True, help="Where to save checkpoints") + p.add_argument("--epochs", type=int, default=30) + p.add_argument("--batch_size", type=int, default=64) + p.add_argument("--lr", type=float, default=2e-5) + p.add_argument("--weight_decay",type=float, default=0.01) + p.add_argument("--warmup_steps",type=int, default=500) + p.add_argument("--grad_accum", type=int, default=1) + p.add_argument("--workers", type=int, default=4) + p.add_argument("--patience", type=int, default=5) + p.add_argument("--seed", type=int, default=42) + p.add_argument("--amp", action="store_true", default=False) + p.add_argument("--wandb", action="store_true", default=False) + p.add_argument("--resume", default=None, help="Resume from checkpoint path") + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/training/phase2_coherence/__init__.py b/training/phase2_coherence/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/training/phase2_coherence/integrate.py b/training/phase2_coherence/integrate.py new file mode 100644 index 0000000000000000000000000000000000000000..5684fd8c690307cc90c83c7f05b5186b1ed1cf52 --- /dev/null +++ b/training/phase2_coherence/integrate.py @@ -0,0 +1,86 @@ +""" +training/phase2_coherence/integrate.py + +Post-training integration for the Coherence engine. +Validates checkpoint, copies to production path, prints integration steps. + +Usage: + python training/phase2_coherence/integrate.py \ + --ckpt models/checkpoints/coherence/best.pt +""" +from __future__ import annotations + +import argparse +import json +import shutil +from datetime import date +from pathlib import Path + +import torch + + +def main(args: argparse.Namespace) -> None: + ckpt_path = Path(args.ckpt) + assert ckpt_path.exists(), f"Checkpoint not found: {ckpt_path}" + + ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=True) + val_auc = ckpt.get("val_auc", "unknown") + epoch = ckpt.get("epoch", "unknown") + + print(f"\n=== Coherence checkpoint ===") + print(f" Epoch: {epoch}") + print(f" Val AUC: {val_auc}") + + import sys + sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + from training.phase2_coherence.model import CoherenceModel + + model = CoherenceModel() + model.load_state_dict(ckpt["model_state"]) + model.eval() + print(" Model loaded OK.") + + # Verify forward pass + dummy_lips = torch.randn(1, 25, 3, 64, 96) + dummy_mfcc = torch.randn(1, 40, 50) + with torch.no_grad(): + out = model(dummy_lips, dummy_mfcc) + assert "sync_score" in out and out["sync_score"].shape == (1,) + print(" Forward pass OK.") + + prod_dir = Path("models/checkpoints/coherence") + prod_dir.mkdir(parents=True, exist_ok=True) + prod_path = prod_dir / "production.pt" + shutil.copy2(ckpt_path, prod_path) + + readme = prod_dir / "README.txt" + with open(readme, "w") as f: + f.write(f"GenAI-DeepDetect Coherence Engine Checkpoint\n") + f.write(f"Training date: {date.today()}\n") + f.write(f"Val AUC: {val_auc}\n") + f.write(f"Epoch: {epoch}\n") + + print(f"\n Copied to: {prod_path}") + print(f""" +=== Integration steps === + +1. The detector auto-loads from: + models/checkpoints/coherence/best.pt + models/checkpoints/coherence/production.pt + +2. Test: + python -c "from src.engines.coherence.detector import CoherenceDetector; d = CoherenceDetector(); print('Loaded')" + +3. Run integration tests: + python training/phase6_export/integration_test.py +""") + + +def parse_args(): + p = argparse.ArgumentParser() + p.add_argument("--ckpt", default="models/checkpoints/coherence/best.pt") + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/training/phase2_coherence/model.py b/training/phase2_coherence/model.py new file mode 100644 index 0000000000000000000000000000000000000000..3d8f11c66edf89ff6ab5e0d441b0062b942f759b --- /dev/null +++ b/training/phase2_coherence/model.py @@ -0,0 +1,169 @@ +""" +training/phase2_coherence/model.py + +Dual-stream lip-audio coherence model for video deepfake detection. + +Architecture basis: LipFD (NeurIPS 2024) + - Visual stream: TimeDistributed ResNet-18 → Temporal Transformer + - Audio stream: 1D-CNN over MFCC features + - Fusion: Concatenate → MLP → sync_score in [0, 1] + +Output: sync_score — 1.0 = synced (real), 0.0 = unsynced (fake) +""" + +from __future__ import annotations + +import torch +import torch.nn as nn +import torchvision.models as tvm + + +class VisualStream(nn.Module): + """ + Processes a sequence of lip ROI frames. + Input: (B, T, C, H, W) — B batch, T=25 frames, C=3, H=64, W=96 + Output: (B, 256) — temporal summary embedding + """ + + def __init__(self, out_dim: int = 256) -> None: + super().__init__() + # ResNet-18 truncated at layer3 (output channels = 256) + rn = tvm.resnet18(weights=None) + self.frame_encoder = nn.Sequential( + rn.conv1, rn.bn1, rn.relu, rn.maxpool, + rn.layer1, rn.layer2, rn.layer3, # (B, 256, h, w) + ) + self.pool = nn.AdaptiveAvgPool2d(1) # → (B, 256, 1, 1) + + # Temporal Transformer — attends over 25 frame embeddings + encoder_layer = nn.TransformerEncoderLayer( + d_model=256, nhead=4, dim_feedforward=512, + dropout=0.1, batch_first=True, + ) + self.temporal_transformer = nn.TransformerEncoder(encoder_layer, num_layers=2) + self.cls_token = nn.Parameter(torch.zeros(1, 1, 256)) + nn.init.normal_(self.cls_token, std=0.02) + + self.proj = nn.Linear(256, out_dim) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + B, T, C, H, W = x.shape + # Encode each frame independently + frames = x.view(B * T, C, H, W) + feat = self.frame_encoder(frames) # (B*T, 256, h, w) + feat = self.pool(feat).squeeze(-1).squeeze(-1) # (B*T, 256) + feat = feat.view(B, T, 256) # (B, T, 256) + + # Prepend [CLS] token + cls = self.cls_token.expand(B, -1, -1) + feat = torch.cat([cls, feat], dim=1) # (B, T+1, 256) + + out = self.temporal_transformer(feat) # (B, T+1, 256) + summary = out[:, 0, :] # [CLS] token → (B, 256) + return self.proj(summary) # (B, out_dim) + + +class AudioStream(nn.Module): + """ + Processes MFCC features. + Input: (B, 40, T) — 40 MFCC coefficients, T time frames + Output: (B, 256) + """ + + def __init__(self, out_dim: int = 256) -> None: + super().__init__() + self.net = nn.Sequential( + nn.Conv1d(40, 128, kernel_size=3, padding=1), + nn.BatchNorm1d(128), + nn.ReLU(inplace=True), + nn.Conv1d(128, 256, kernel_size=3, padding=1), + nn.BatchNorm1d(256), + nn.ReLU(inplace=True), + nn.Conv1d(256, 256, kernel_size=3, padding=1), + nn.BatchNorm1d(256), + nn.ReLU(inplace=True), + nn.AdaptiveAvgPool1d(1), # global average pool → (B, 256, 1) + ) + self.proj = nn.Linear(256, out_dim) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + feat = self.net(x).squeeze(-1) # (B, 256) + return self.proj(feat) # (B, out_dim) + + +class CoherenceModel(nn.Module): + """ + Full dual-stream coherence model. + + forward(lip_frames, mfcc) returns: + { + 'sync_score': Tensor (B,) — 1=synced, 0=unsynced + 'visual_embed': Tensor (B, 256) + 'audio_embed': Tensor (B, 256) + } + + Use sync_logit (before sigmoid) with BCEWithLogitsLoss during training. + At inference: sync_score = sigmoid(sync_logit) + """ + + def __init__(self, embed_dim: int = 256, dropout: float = 0.3) -> None: + super().__init__() + self.visual_stream = VisualStream(out_dim=embed_dim) + self.audio_stream = AudioStream(out_dim=embed_dim) + + self.classifier = nn.Sequential( + nn.Linear(embed_dim * 2, 256), + nn.ReLU(inplace=True), + nn.Dropout(dropout), + nn.Linear(256, 1), # logit — apply sigmoid externally + ) + + def forward( + self, + lip_frames: torch.Tensor, # (B, T, C, H, W) + mfcc: torch.Tensor, # (B, 40, T_audio) + ) -> dict[str, torch.Tensor]: + v_embed = self.visual_stream(lip_frames) # (B, 256) + a_embed = self.audio_stream(mfcc) # (B, 256) + + fused = torch.cat([v_embed, a_embed], dim=1) # (B, 512) + logit = self.classifier(fused).squeeze(-1) # (B,) + + return { + "sync_score": torch.sigmoid(logit), + "sync_logit": logit, + "visual_embed": v_embed, + "audio_embed": a_embed, + } + + +class CoherenceLoss(nn.Module): + """BCE + optional contrastive loss between visual/audio embeddings.""" + + def __init__(self, contrastive_weight: float = 0.1) -> None: + super().__init__() + self.contrastive_weight = contrastive_weight + self.bce = nn.BCEWithLogitsLoss() + + def forward( + self, + outputs: dict[str, torch.Tensor], + labels: torch.Tensor, # 1=synced(real), 0=unsynced(fake) + ) -> dict[str, torch.Tensor]: + bce_loss = self.bce(outputs["sync_logit"], labels.float()) + + # Cosine contrastive: pull real pairs together, push fake apart + cos_sim = nn.functional.cosine_similarity( + outputs["visual_embed"], outputs["audio_embed"] + ) + # Real: cos_sim should be high (→ 1), Fake: should be low (→ -1) + # Loss: for real samples maximise similarity, for fake minimise + contrastive = (labels * (1 - cos_sim) + (1 - labels) * torch.clamp(cos_sim + 0.5, min=0)).mean() + + total = bce_loss + self.contrastive_weight * contrastive + + return { + "total": total, + "bce": bce_loss, + "contrastive": contrastive, + } diff --git a/training/phase2_coherence/preprocess_videos.py b/training/phase2_coherence/preprocess_videos.py new file mode 100644 index 0000000000000000000000000000000000000000..6c35435a94a9371a6a35c0c99c8bb46f76f4fe5e --- /dev/null +++ b/training/phase2_coherence/preprocess_videos.py @@ -0,0 +1,324 @@ +""" +training/phase2_coherence/preprocess_videos.py + +Extracts lip-audio clip pairs from video datasets for Coherence engine training. + +Output: data/processed/coherence/{split}/{label}/{video_id}_{clip_idx}.npz + Each .npz contains: + lip_frames: (25, 96, 64, 3) uint8 + mfcc: (40, T) float32 + label: int 1=synced/real, 0=unsynced/fake + source: str dataset name + +Run: + python training/phase2_coherence/preprocess_videos.py \ + --ff_root /kaggle/input/faceforensics-in-compressed-videos \ + --dfdc_root /kaggle/input/deepfake-detection-challenge \ + --output_dir /kaggle/working/processed/coherence +""" + +from __future__ import annotations + +import argparse +import json +import logging +import multiprocessing as mp +import random +import time +from pathlib import Path + +import cv2 +import librosa +import numpy as np +from tqdm import tqdm + +# ── Logging ─────────────────────────────────────────────────────────────────── + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s %(message)s", + datefmt="%H:%M:%S", +) +log = logging.getLogger(__name__) + +# ── Constants ───────────────────────────────────────────────────────────────── + +SAMPLE_RATE = 16_000 +N_MFCC = 40 +CLIP_FRAMES = 25 # 1 second at 25fps +LIP_H, LIP_W = 64, 96 +TARGET_FPS = 25 +MIN_VIDEO_HEIGHT = 240 + +face_cascade = cv2.CascadeClassifier( + cv2.data.haarcascades + "haarcascade_frontalface_default.xml" +) + + +# ── Video processing ────────────────────────────────────────────────────────── + +def extract_audio(video_path: Path) -> np.ndarray | None: + """Extract mono audio at 16kHz using librosa.""" + try: + import subprocess, tempfile, os + # Extract audio to temp wav using ffmpeg + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: + tmp_path = tmp.name + result = subprocess.run( + ["ffmpeg", "-y", "-i", str(video_path), "-ac", "1", + "-ar", str(SAMPLE_RATE), tmp_path], + capture_output=True, timeout=60 + ) + if result.returncode != 0: + return None + audio, _ = librosa.load(tmp_path, sr=SAMPLE_RATE, mono=True) + os.unlink(tmp_path) + return audio + except Exception: + return None + + +def extract_lip_frames(cap: cv2.VideoCapture, start_frame: int) -> np.ndarray | None: + """ + Extract 25 lip ROI frames starting at start_frame. + Returns (25, 96, 64, 3) uint8 or None if detection fails. + """ + cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame) + frames = [] + + for _ in range(CLIP_FRAMES): + ret, frame = cap.read() + if not ret: + return None + + if frame.shape[0] < MIN_VIDEO_HEIGHT: + return None + + gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=4) + + if len(faces) == 0: + frames.append(None) + continue + + # Take the largest face + x, y, w, h = max(faces, key=lambda f: f[2] * f[3]) + # Lip ROI: lower 1/3 of face bounding box + lip_y = y + int(h * 0.6) + lip_h_crop = int(h * 0.4) + lip_roi = frame[lip_y:lip_y + lip_h_crop, x:x + w] + + if lip_roi.size == 0: + frames.append(None) + continue + + lip_roi = cv2.resize(lip_roi, (LIP_W, LIP_H)) + lip_roi = cv2.cvtColor(lip_roi, cv2.COLOR_BGR2RGB) + frames.append(lip_roi) + + # Require ≤20% missing frames + missing = sum(1 for f in frames if f is None) + if missing > CLIP_FRAMES * 0.2: + return None + + # Fill missing frames with nearest neighbour + for i in range(len(frames)): + if frames[i] is None: + for j in range(i - 1, -1, -1): + if frames[j] is not None: + frames[i] = frames[j] + break + if frames[i] is None: + frames[i] = np.zeros((LIP_H, LIP_W, 3), dtype=np.uint8) + + return np.stack(frames, axis=0).astype(np.uint8) # (25, 64, 96, 3) + + +def process_video(args_tuple) -> list[dict]: + """Process a single video and return extracted clip metadata.""" + video_path, label, source, output_dir, max_clips, seed = args_tuple + rng = random.Random(seed) + clips = [] + + cap = cv2.VideoCapture(str(video_path)) + if not cap.isOpened(): + return clips + + total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + fps = cap.get(cv2.CAP_PROP_FPS) or TARGET_FPS + + if total_frames < CLIP_FRAMES * 2: + cap.release() + return clips + + # Extract audio for the whole video + audio = extract_audio(video_path) + if audio is None: + cap.release() + return clips + + # Sample clip start positions + max_start = total_frames - CLIP_FRAMES + step = max(1, max_start // (max_clips * 2)) + candidates = list(range(0, max_start, step)) + rng.shuffle(candidates) + + video_id = video_path.stem + + for start_frame in candidates[:max_clips * 2]: + if len(clips) >= max_clips: + break + + lip_frames = extract_lip_frames(cap, start_frame) + if lip_frames is None: + continue + + # Corresponding audio segment + start_sec = start_frame / fps + audio_start = int(start_sec * SAMPLE_RATE) + audio_end = audio_start + SAMPLE_RATE # 1 second + if audio_end > len(audio): + continue + segment = audio[audio_start:audio_end] + + mfcc = librosa.feature.mfcc(y=segment, sr=SAMPLE_RATE, n_mfcc=N_MFCC) + + # Save + clip_idx = len(clips) + label_str = "real" if label == 1 else "fake" + out_path = output_dir / label_str / f"{video_id}_{clip_idx:04d}.npz" + out_path.parent.mkdir(parents=True, exist_ok=True) + + np.savez_compressed( + out_path, + lip_frames=lip_frames, + mfcc=mfcc.astype(np.float32), + label=np.array(label, dtype=np.int32), + source=np.array(source), + ) + clips.append({"path": str(out_path), "label": label}) + + cap.release() + return clips + + +# ── Dataset loaders ─────────────────────────────────────────────────────────── + +def collect_ff(ff_root: Path) -> list[tuple[Path, int, str]]: + """FaceForensics++: manipulated_sequences=fake, original_sequences=real.""" + videos = [] + for subfolder in ff_root.rglob("*.mp4"): + parts = [p.lower() for p in subfolder.parts] + label = 0 if "manipulated" in str(subfolder).lower() else 1 + videos.append((subfolder, label, "ff++")) + log.info(f"FF++: {len(videos)} videos") + return videos + + +def collect_dfdc(dfdc_root: Path) -> list[tuple[Path, int, str]]: + """DFDC: use metadata.json per chunk folder.""" + videos = [] + for metadata_file in dfdc_root.rglob("metadata.json"): + chunk_dir = metadata_file.parent + with open(metadata_file) as f: + meta = json.load(f) + for fname, info in meta.items(): + video_path = chunk_dir / fname + if not video_path.exists(): + continue + label = 1 if info.get("label", "FAKE") == "REAL" else 0 + videos.append((video_path, label, "dfdc")) + log.info(f"DFDC: {len(videos)} videos") + return videos + + +def collect_celebdf(celebdf_root: Path) -> list[tuple[Path, int, str]]: + """Celeb-DF v2: real/ and fake/ folders.""" + videos = [] + for video_path in celebdf_root.rglob("*.mp4"): + parts_lower = str(video_path).lower() + label = 0 if "fake" in parts_lower or "synthesis" in parts_lower else 1 + videos.append((video_path, label, "celebdf")) + log.info(f"CelebDF: {len(videos)} videos") + return videos + + +# ── Split assignment ────────────────────────────────────────────────────────── + +def assign_splits( + video_list: list[tuple[Path, int, str]], + seed: int = 42, + train_ratio: float = 0.80, + val_ratio: float = 0.10, +) -> dict[str, list[tuple[Path, int, str]]]: + rng = random.Random(seed) + shuffled = list(video_list) + rng.shuffle(shuffled) + n = len(shuffled) + n_train = int(n * train_ratio) + n_val = int(n * val_ratio) + return { + "train": shuffled[:n_train], + "val": shuffled[n_train:n_train + n_val], + "test": shuffled[n_train + n_val:], + } + + +# ── Main ───────────────────────────────────────────────────────────────────── + +def main(args: argparse.Namespace) -> None: + output_dir = Path(args.output_dir) + + all_videos: list[tuple[Path, int, str]] = [] + if args.ff_root and Path(args.ff_root).exists(): + all_videos += collect_ff(Path(args.ff_root)) + if args.dfdc_root and Path(args.dfdc_root).exists(): + all_videos += collect_dfdc(Path(args.dfdc_root)) + if args.celebdf_root and Path(args.celebdf_root).exists(): + all_videos += collect_celebdf(Path(args.celebdf_root)) + + if not all_videos: + log.error("No video datasets found — check your paths") + return + + log.info(f"Total videos: {len(all_videos)}") + splits = assign_splits(all_videos, seed=args.seed) + + total_clips = 0 + for split, videos in splits.items(): + split_dir = output_dir / split + split_dir.mkdir(parents=True, exist_ok=True) + + tasks = [ + (v, lbl, src, split_dir, args.max_clips, args.seed + i) + for i, (v, lbl, src) in enumerate(videos) + ] + + log.info(f"Processing {split} split ({len(tasks)} videos) ...") + with mp.Pool(args.workers) as pool: + results = list(tqdm( + pool.imap_unordered(process_video, tasks), + total=len(tasks), desc=split, + )) + + split_clips = sum(len(r) for r in results) + total_clips += split_clips + log.info(f" {split}: {split_clips} clips extracted") + + log.info(f"Done. Total clips: {total_clips}") + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description="Preprocess videos for Coherence engine") + p.add_argument("--ff_root", default=None) + p.add_argument("--dfdc_root", default=None) + p.add_argument("--celebdf_root",default=None) + p.add_argument("--output_dir", default="data/processed/coherence") + p.add_argument("--workers", type=int, default=4) + p.add_argument("--max_clips", type=int, default=20) + p.add_argument("--seed", type=int, default=42) + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/training/phase2_coherence/train.py b/training/phase2_coherence/train.py new file mode 100644 index 0000000000000000000000000000000000000000..90422b99ffdc6a6c15d5f09d29946c92b644d8ab --- /dev/null +++ b/training/phase2_coherence/train.py @@ -0,0 +1,244 @@ +""" +training/phase2_coherence/train.py + +Training script for the Coherence engine (lip-audio sync). + +Kaggle usage: + !python training/phase2_coherence/train.py \ + --data_dir /kaggle/working/processed/coherence \ + --output_dir /kaggle/working/checkpoints/coherence \ + --epochs 25 --batch_size 16 --lr 1e-4 --amp +""" +from __future__ import annotations + +import argparse +import logging +import random +import time +from datetime import datetime +from pathlib import Path + +import numpy as np +import torch +import torch.nn as nn +from torch.cuda.amp import GradScaler, autocast +from torch.utils.data import DataLoader, Dataset +from sklearn.metrics import roc_auc_score, accuracy_score, f1_score +from tqdm import tqdm +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +import sys +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) +from training.phase2_coherence.model import CoherenceModel, CoherenceLoss + + +def setup_logging(output_dir: Path) -> None: + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + log_dir = Path("training/logs") + log_dir.mkdir(parents=True, exist_ok=True) + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s %(message)s", + handlers=[ + logging.StreamHandler(), + logging.FileHandler(log_dir / f"phase2_coherence_{ts}.log"), + ], + ) + +log = logging.getLogger(__name__) + + +class CoherenceDataset(Dataset): + def __init__(self, root_dir: Path, split: str): + self.samples = [] + split_dir = Path(root_dir) / split + for label_str, label in [("real", 1), ("fake", 0)]: + d = split_dir / label_str + if not d.exists(): + continue + for p in sorted(d.glob("*.npz")): + self.samples.append((p, label)) + log.info(f"CoherenceDataset [{split}]: {len(self.samples)} clips") + + def __len__(self) -> int: + return len(self.samples) + + def __getitem__(self, idx: int) -> tuple: + path, label = self.samples[idx] + data = np.load(path) + lip_frames = data["lip_frames"].astype(np.float32) / 255.0 # (25, H, W, 3) → [0,1] + mfcc = data["mfcc"].astype(np.float32) # (40, T) + + # lip_frames to (25, 3, 64, 96) + lip_tensor = torch.tensor(lip_frames).permute(0, 3, 1, 2) # (25, 3, H, W) + mfcc_tensor = torch.tensor(mfcc) + + # Pad/trim MFCC to fixed length 50 + T = mfcc_tensor.shape[1] + if T >= 50: + mfcc_tensor = mfcc_tensor[:, :50] + else: + mfcc_tensor = torch.nn.functional.pad(mfcc_tensor, (0, 50 - T)) + + return lip_tensor, mfcc_tensor, torch.tensor(label, dtype=torch.float32) + + +def train_epoch(model, loader, criterion, optimizer, scaler, device, args) -> dict: + model.train() + total_loss = 0.0 + all_preds, all_labels = [], [] + + for lip, mfcc, labels in tqdm(loader, desc="Train", leave=False): + lip, mfcc, labels = lip.to(device), mfcc.to(device), labels.to(device) + + with autocast(enabled=args.amp): + out = model(lip, mfcc) + losses = criterion(out, labels) + + scaler.scale(losses["total"]).backward() + scaler.unscale_(optimizer) + nn.utils.clip_grad_norm_(model.parameters(), 1.0) + scaler.step(optimizer) + scaler.update() + optimizer.zero_grad() + + total_loss += losses["total"].item() + preds = (out["sync_score"].detach().cpu().numpy() > 0.5).astype(int) + all_preds.extend(preds) + all_labels.extend(labels.cpu().numpy().astype(int)) + + return { + "loss": total_loss / len(loader), + "accuracy": accuracy_score(all_labels, all_preds), + } + + +@torch.no_grad() +def validate(model, loader, criterion, device, args, epoch: int, output_dir: Path) -> dict: + model.eval() + total_loss = 0.0 + all_scores, all_preds, all_labels = [], [], [] + + for lip, mfcc, labels in tqdm(loader, desc="Val", leave=False): + lip, mfcc, labels = lip.to(device), mfcc.to(device), labels.to(device) + with autocast(enabled=args.amp): + out = model(lip, mfcc) + losses = criterion(out, labels) + total_loss += losses["total"].item() + scores = out["sync_score"].cpu().numpy() + preds = (scores > 0.5).astype(int) + all_scores.extend(scores) + all_preds.extend(preds) + all_labels.extend(labels.cpu().numpy().astype(int)) + + auc = roc_auc_score(all_labels, all_scores) if len(set(all_labels)) > 1 else 0.5 + + # EER computation + from sklearn.metrics import roc_curve + fpr, tpr, thresholds = roc_curve(all_labels, all_scores) + eer_idx = np.argmin(np.abs(fpr - (1 - tpr))) + eer = float(fpr[eer_idx]) + + # Save ROC plot + plt.figure(figsize=(6, 4)) + plt.plot(fpr, tpr, label=f"AUC={auc:.3f}") + plt.xlabel("FPR"); plt.ylabel("TPR"); plt.title(f"ROC Epoch {epoch+1}") + plt.legend(); plt.tight_layout() + plt.savefig(output_dir / f"roc_epoch_{epoch+1:02d}.png") + plt.close() + + return { + "loss": total_loss / len(loader), + "auc": auc, + "eer": eer, + "accuracy": accuracy_score(all_labels, all_preds), + "f1": f1_score(all_labels, all_preds, zero_division=0), + } + + +def save_checkpoint(path, model, optimizer, scaler, epoch, val_auc, args): + torch.save({ + "epoch": epoch, + "model_state": model.state_dict(), + "optimizer": optimizer.state_dict(), + "scaler": scaler.state_dict(), + "val_auc": val_auc, + "config": vars(args), + }, path) + + +def main(args: argparse.Namespace) -> None: + setup_logging(Path(args.output_dir)) + torch.manual_seed(args.seed) + np.random.seed(args.seed) + random.seed(args.seed) + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + train_ds = CoherenceDataset(args.data_dir, "train") + val_ds = CoherenceDataset(args.data_dir, "val") + train_dl = DataLoader(train_ds, batch_size=args.batch_size, shuffle=True, + num_workers=args.workers, pin_memory=True) + val_dl = DataLoader(val_ds, batch_size=args.batch_size * 2, shuffle=False, + num_workers=args.workers, pin_memory=True) + + model = CoherenceModel().to(device) + criterion = CoherenceLoss(contrastive_weight=args.contrastive_weight) + optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr, + weight_decay=args.weight_decay) + scaler = GradScaler(enabled=args.amp) + + best_auc = 0.0 + no_improve = 0 + + for epoch in range(args.epochs): + t0 = time.time() + train_m = train_epoch(model, train_dl, criterion, optimizer, scaler, device, args) + val_m = validate(model, val_dl, criterion, device, args, epoch, output_dir) + + log.info( + f"Epoch {epoch+1:02d}/{args.epochs} | " + f"train_loss={train_m['loss']:.4f} | " + f"val_auc={val_m['auc']:.4f} | EER={val_m['eer']:.3f} | " + f"{time.time()-t0:.0f}s" + ) + + save_checkpoint(output_dir / "latest.pt", model, optimizer, scaler, epoch, val_m["auc"], args) + + if val_m["auc"] > best_auc: + best_auc = val_m["auc"] + no_improve = 0 + save_checkpoint(output_dir / "best.pt", model, optimizer, scaler, epoch, best_auc, args) + log.info(f" New best AUC: {best_auc:.4f}") + else: + no_improve += 1 + if no_improve >= args.patience: + log.info(f"Early stopping at epoch {epoch+1}") + break + + log.info(f"Done. Best AUC: {best_auc:.4f}") + + +def parse_args(): + p = argparse.ArgumentParser(description="Train Coherence engine (Phase 2)") + p.add_argument("--data_dir", required=True) + p.add_argument("--output_dir", required=True) + p.add_argument("--epochs", type=int, default=25) + p.add_argument("--batch_size", type=int, default=16) + p.add_argument("--lr", type=float, default=1e-4) + p.add_argument("--weight_decay", type=float, default=1e-4) + p.add_argument("--contrastive_weight",type=float, default=0.1) + p.add_argument("--workers", type=int, default=4) + p.add_argument("--patience", type=int, default=5) + p.add_argument("--seed", type=int, default=42) + p.add_argument("--amp", action="store_true", default=False) + p.add_argument("--resume", default=None) + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/training/phase3_sstgnn/__init__.py b/training/phase3_sstgnn/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/training/phase3_sstgnn/build_graph.py b/training/phase3_sstgnn/build_graph.py new file mode 100644 index 0000000000000000000000000000000000000000..5092281f58e1f4b9c19d339bf73819f03910254b --- /dev/null +++ b/training/phase3_sstgnn/build_graph.py @@ -0,0 +1,161 @@ +""" +training/phase3_sstgnn/build_graph.py + +Converts a (T, L, 3) landmark sequence into a torch_geometric Data object. + +Graph structure: + Nodes: T × L = up to 4096 nodes + Features: (x, y, z, frame_idx/T, landmark_idx/L) → 5-dim + + Edges: + 1. SPATIAL — anatomically adjacent landmarks within each frame (weight 1.0) + 2. TEMPORAL — each landmark connects to same landmark in next 3 frames + (weight = exp(-|frame_diff|)) + 3. SELF — each node → itself (required for GAT residual) + +Copy to src/engines/sstgnn/graph_builder.py for inference use. +""" + +from __future__ import annotations + +import math +from typing import Tuple + +import numpy as np +import torch + +try: + from torch_geometric.data import Data + TORCH_GEOMETRIC_AVAILABLE = True +except ImportError: + TORCH_GEOMETRIC_AVAILABLE = False + + +# ── Anatomical adjacency (68-landmark subset, MediaPipe-derived) ────────────── + +def _build_adjacency_list() -> list[tuple[int, int]]: + """Edges between anatomically adjacent landmarks on 68-landmark face.""" + edges = [] + + # Jaw contour (0–16) + edges += [(i, i + 1) for i in range(16)] + + # Right eyebrow (17–21) + edges += [(i, i + 1) for i in range(17, 21)] + + # Left eyebrow (22–26) + edges += [(i, i + 1) for i in range(22, 26)] + + # Nose bridge (27–30) + edges += [(i, i + 1) for i in range(27, 30)] + + # Nose base (31–35) + edges += [(i, i + 1) for i in range(31, 35)] + edges += [(30, 35)] # connect bridge to base + + # Right eye (36–41) — closed loop + edges += [(36, 37), (37, 38), (38, 39), (39, 40), (40, 41), (41, 36)] + + # Left eye (42–47) — closed loop + edges += [(42, 43), (43, 44), (44, 45), (45, 46), (46, 47), (47, 42)] + + # Mouth outer (48–59) — closed loop + edges += [(i, i + 1) for i in range(48, 59)] + edges += [(59, 48)] + + # Mouth inner (60–67) — closed loop + edges += [(i, i + 1) for i in range(60, 67)] + edges += [(67, 60)] + + return edges + + +SPATIAL_EDGES = _build_adjacency_list() # ~51 pairs +TEMPORAL_WINDOW = 3 # connect to next N frames + + +def build_temporal_graph( + landmark_sequence: np.ndarray, # (T, L, 3) + label: int = -1, +) -> "Data": + """ + Build a spatio-temporal graph from a landmark sequence. + + Args: + landmark_sequence: (T, L, 3) — T frames, L landmarks, xyz normalized [0,1] + label: binary label for training (-1 for inference) + + Returns: + torch_geometric.data.Data with x, edge_index, edge_attr, y + """ + if not TORCH_GEOMETRIC_AVAILABLE: + raise ImportError("torch_geometric is required. See KAGGLE.md for install.") + + T, L, _ = landmark_sequence.shape + + # ── Node features ───────────────────────────────────────────────────────── + # 5-dim: (x, y, z, frame_idx/T, landmark_idx/L) + node_feats = [] + for t in range(T): + for l_idx in range(L): + x, y, z = landmark_sequence[t, l_idx] + node_feats.append([ + float(x), + float(y), + float(z), + t / max(T - 1, 1), + l_idx / max(L - 1, 1), + ]) + + node_tensor = torch.tensor(node_feats, dtype=torch.float32) # (T*L, 5) + + # ── Edge construction ───────────────────────────────────────────────────── + src_nodes, dst_nodes, weights = [], [], [] + + # Node index helper: frame t, landmark l → global node idx + def node_id(t: int, l: int) -> int: + return t * L + l + + # 1. SPATIAL edges — same frame, anatomically adjacent landmarks + for t in range(T): + for (i, j) in SPATIAL_EDGES: + if i < L and j < L: + u, v = node_id(t, i), node_id(t, j) + # Add both directions + src_nodes.extend([u, v]) + dst_nodes.extend([v, u]) + weights.extend([1.0, 1.0]) + + # 2. TEMPORAL edges — same landmark across adjacent frames + for t in range(T): + for dt in range(1, TEMPORAL_WINDOW + 1): + t2 = t + dt + if t2 >= T: + break + w = math.exp(-dt) # exponential decay + for l_idx in range(L): + u, v = node_id(t, l_idx), node_id(t2, l_idx) + src_nodes.extend([u, v]) + dst_nodes.extend([v, u]) + weights.extend([w, w]) + + # 3. SELF edges + for n in range(T * L): + src_nodes.append(n) + dst_nodes.append(n) + weights.append(1.0) + + edge_index = torch.tensor([src_nodes, dst_nodes], dtype=torch.long) + edge_attr = torch.tensor(weights, dtype=torch.float32).unsqueeze(1) # (E, 1) + + y = torch.tensor([label], dtype=torch.float32) if label >= 0 else None + + data = Data( + x=node_tensor, + edge_index=edge_index, + edge_attr=edge_attr, + ) + if y is not None: + data.y = y + + return data diff --git a/training/phase3_sstgnn/extract_landmarks.py b/training/phase3_sstgnn/extract_landmarks.py new file mode 100644 index 0000000000000000000000000000000000000000..900d6f17f120b6dee1c7358a3c416361f2255c0c --- /dev/null +++ b/training/phase3_sstgnn/extract_landmarks.py @@ -0,0 +1,265 @@ +""" +training/phase3_sstgnn/extract_landmarks.py + +Extracts 68-landmark sequences from video datasets for SSTGNN training. + +Output: data/processed/sstgnn/{split}/{label}/{video_id}_{window_idx}.npy + Shape: (64, 68, 3) — 64 frames, 68 landmarks, xyz normalized [0,1] + +Key fixes applied vs original spec: + - min_detection_confidence=0.3 (was 0.5 — too strict for DFDC low-res) + - min_tracking_confidence=0.3 + - Skip frames where video height < 240px + +Run: + python training/phase3_sstgnn/extract_landmarks.py \ + --ff_root /kaggle/input/faceforensics-in-compressed-videos \ + --dfdc_root /kaggle/input/deepfake-detection-challenge \ + --output_dir /kaggle/working/processed/sstgnn \ + --workers 4 +""" + +from __future__ import annotations + +import argparse +import json +import logging +import multiprocessing as mp +import random +from pathlib import Path + +import cv2 +import numpy as np +import mediapipe as mp_lib +from tqdm import tqdm + +# ── Logging ─────────────────────────────────────────────────────────────────── + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s %(message)s", + datefmt="%H:%M:%S", +) +log = logging.getLogger(__name__) + +# ── Constants ───────────────────────────────────────────────────────────────── + +WINDOW_FRAMES = 64 +WINDOW_STRIDE = 32 +NUM_LANDMARKS = 68 +MIN_VIDEO_HEIGHT = 240 + +# 68-landmark indices from MediaPipe 468-point mesh +# Selected to match the standard 68-point dlib subset +KEY_LANDMARKS = [ + 162, 234, 93, 58, 172, 136, 149, 148, 152, 377, 378, 365, 397, 288, 323, 454, + 389, 71, 63, 105, 66, 107, 336, 296, 334, 293, 301, 168, 197, 5, 4, 75, 97, + 2, 326, 305, 33, 160, 158, 133, 153, 144, 362, 385, 387, 263, 373, 380, 61, + 39, 37, 0, 267, 269, 291, 405, 314, 17, 84, 181, 78, 82, 13, 312, 308, 317, + 14, 87, +][:NUM_LANDMARKS] + + +def extract_landmarks_from_video(args_tuple) -> list[dict]: + """Worker function — processes a single video.""" + video_path, label, source, output_dir, seed = args_tuple + rng = random.Random(seed) + results = [] + + cap = cv2.VideoCapture(str(video_path)) + if not cap.isOpened(): + return results + + total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + if total_frames < WINDOW_FRAMES: + cap.release() + return results + + # Initialise MediaPipe with lowered confidence thresholds + mp_face_mesh = mp_lib.solutions.face_mesh + face_mesh = mp_face_mesh.FaceMesh( + static_image_mode=False, + max_num_faces=1, + refine_landmarks=False, + min_detection_confidence=0.3, # FIX: was 0.5 + min_tracking_confidence=0.3, # FIX: was 0.5 + ) + + all_frame_landmarks = [] # per-frame landmark arrays or None + + frame_idx = 0 + while True: + ret, frame = cap.read() + if not ret: + break + + # Skip low-res frames + if frame.shape[0] < MIN_VIDEO_HEIGHT: + all_frame_landmarks.append(None) + frame_idx += 1 + continue + + rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + result = face_mesh.process(rgb) + + if result.multi_face_landmarks: + lms = result.multi_face_landmarks[0].landmark + coords = np.array([ + [lms[i].x, lms[i].y, lms[i].z] + for i in KEY_LANDMARKS + ], dtype=np.float32) # (68, 3) + all_frame_landmarks.append(coords) + else: + all_frame_landmarks.append(None) + + frame_idx += 1 + + cap.release() + face_mesh.close() + + # Extract windows with stride + video_id = video_path.stem + label_str = "real" if label == 1 else "fake" + + for w_start in range(0, len(all_frame_landmarks) - WINDOW_FRAMES + 1, WINDOW_STRIDE): + window = all_frame_landmarks[w_start:w_start + WINDOW_FRAMES] + + # Skip windows with >20% missing detections + missing = sum(1 for f in window if f is None) + if missing > WINDOW_FRAMES * 0.2: + continue + + # Fill missing frames with nearest neighbour + filled = list(window) + for i in range(len(filled)): + if filled[i] is None: + for j in range(i - 1, -1, -1): + if filled[j] is not None: + filled[i] = filled[j] + break + if filled[i] is None: + filled[i] = np.zeros((NUM_LANDMARKS, 3), dtype=np.float32) + + seq = np.stack(filled, axis=0) # (64, 68, 3) + assert seq.shape == (WINDOW_FRAMES, NUM_LANDMARKS, 3) + + w_idx = len(results) + out_path = output_dir / label_str / f"{video_id}_{w_idx:04d}.npy" + out_path.parent.mkdir(parents=True, exist_ok=True) + np.save(out_path, seq) + results.append({"path": str(out_path), "label": label}) + + return results + + +# ── Dataset collectors (same as coherence) ─────────────────────────────────── + +def collect_ff(ff_root: Path) -> list[tuple[Path, int, str]]: + videos = [] + for p in ff_root.rglob("*.mp4"): + label = 0 if "manipulated" in str(p).lower() else 1 + videos.append((p, label, "ff++")) + log.info(f"FF++: {len(videos)} videos") + return videos + + +def collect_dfdc(dfdc_root: Path) -> list[tuple[Path, int, str]]: + videos = [] + for meta_file in dfdc_root.rglob("metadata.json"): + chunk_dir = meta_file.parent + with open(meta_file) as f: + meta = json.load(f) + for fname, info in meta.items(): + p = chunk_dir / fname + if not p.exists(): + continue + label = 1 if info.get("label", "FAKE") == "REAL" else 0 + videos.append((p, label, "dfdc")) + log.info(f"DFDC: {len(videos)} videos") + return videos + + +def collect_celebdf(celebdf_root: Path) -> list[tuple[Path, int, str]]: + videos = [] + for p in celebdf_root.rglob("*.mp4"): + label = 0 if "fake" in str(p).lower() else 1 + videos.append((p, label, "celebdf")) + log.info(f"CelebDF: {len(videos)} videos") + return videos + + +def assign_splits(videos, seed=42): + rng = random.Random(seed) + shuffled = list(videos) + rng.shuffle(shuffled) + n = len(shuffled) + n_train = int(n * 0.80) + n_val = int(n * 0.10) + return { + "train": shuffled[:n_train], + "val": shuffled[n_train:n_train + n_val], + "test": shuffled[n_train + n_val:], + } + + +# ── Main ───────────────────────────────────────────────────────────────────── + +def main(args: argparse.Namespace) -> None: + output_dir = Path(args.output_dir) + + all_videos = [] + if args.ff_root and Path(args.ff_root).exists(): + all_videos += collect_ff(Path(args.ff_root)) + if args.dfdc_root and Path(args.dfdc_root).exists(): + all_videos += collect_dfdc(Path(args.dfdc_root)) + if args.celebdf_root and Path(args.celebdf_root).exists(): + all_videos += collect_celebdf(Path(args.celebdf_root)) + + if not all_videos: + log.error("No videos found. Check dataset paths.") + return + + log.info(f"Total videos: {len(all_videos)}") + splits = assign_splits(all_videos, seed=args.seed) + + total_windows = 0 + failed = 0 + + for split, videos in splits.items(): + split_dir = output_dir / split + split_dir.mkdir(parents=True, exist_ok=True) + + tasks = [ + (v, lbl, src, split_dir, args.seed + i) + for i, (v, lbl, src) in enumerate(videos) + ] + + log.info(f"Extracting {split} landmarks ({len(tasks)} videos) ...") + with mp.Pool(args.workers) as pool: + results = list(tqdm( + pool.imap_unordered(extract_landmarks_from_video, tasks), + total=len(tasks), desc=split, + )) + + split_windows = sum(len(r) for r in results) + total_windows += split_windows + failed += sum(1 for r in results if len(r) == 0) + log.info(f" {split}: {split_windows} landmark windows") + + fail_pct = 100 * failed / max(len(all_videos), 1) + log.info(f"Done. Total windows: {total_windows} | Failed videos: {failed} ({fail_pct:.1f}%)") + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description="Extract landmarks for SSTGNN") + p.add_argument("--ff_root", default=None) + p.add_argument("--dfdc_root", default=None) + p.add_argument("--celebdf_root",default=None) + p.add_argument("--output_dir", default="data/processed/sstgnn") + p.add_argument("--workers", type=int, default=4) + p.add_argument("--seed", type=int, default=42) + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/training/phase3_sstgnn/integrate.py b/training/phase3_sstgnn/integrate.py new file mode 100644 index 0000000000000000000000000000000000000000..de95b9710f1637769cae102fe245559fa12f0c34 --- /dev/null +++ b/training/phase3_sstgnn/integrate.py @@ -0,0 +1,86 @@ +""" +training/phase3_sstgnn/integrate.py + +Post-training integration for the SSTGNN engine. +State dict only — no TorchScript. + +Usage: + python training/phase3_sstgnn/integrate.py \ + --ckpt models/checkpoints/sstgnn/best.pt +""" +from __future__ import annotations + +import argparse +import shutil +from datetime import date +from pathlib import Path + +import torch + + +def main(args: argparse.Namespace) -> None: + ckpt_path = Path(args.ckpt) + assert ckpt_path.exists(), f"Checkpoint not found: {ckpt_path}" + + ckpt = torch.load(ckpt_path, map_location="cpu") + val_auc = ckpt.get("val_auc", "unknown") + epoch = ckpt.get("epoch", "unknown") + + print(f"\n=== SSTGNN checkpoint ===") + print(f" Epoch: {epoch}") + print(f" Val AUC: {val_auc}") + + import sys + sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + from training.phase3_sstgnn.model import SSTGNNModel + + model = SSTGNNModel() + key = "state_dict" if "state_dict" in ckpt else "model_state" + model.load_state_dict(ckpt[key]) + model.eval() + print(" Model loaded OK.") + print(" NOTE: SSTGNN uses state_dict only — no TorchScript export.") + + prod_dir = Path("models/checkpoints/sstgnn") + prod_dir.mkdir(parents=True, exist_ok=True) + prod_path = prod_dir / "production.pt" + + # Save in state_dict format for detector compatibility + torch.save({ + "state_dict": model.state_dict(), + "model_config": model.config, + "val_auc": val_auc, + "epoch": epoch, + }, prod_path) + + readme = prod_dir / "README.txt" + with open(readme, "w") as f: + f.write(f"GenAI-DeepDetect SSTGNN Engine Checkpoint\n") + f.write(f"Training date: {date.today()}\n") + f.write(f"Val AUC: {val_auc}\n") + f.write(f"Epoch: {epoch}\n") + f.write(f"Format: state_dict (load with model.load_state_dict)\n") + + print(f"\n Saved to: {prod_path}") + print(f""" +=== Integration steps === + +1. SSTGNNDetector auto-loads state_dict from: + models/checkpoints/sstgnn/best.pt + models/checkpoints/sstgnn/production.pt + +2. The detector uses load_state_dict — never torch.jit.load. + +3. Test: + python -c "from src.engines.sstgnn.detector import SSTGNNDetector; d = SSTGNNDetector(); print('Loaded')" +""") + + +def parse_args(): + p = argparse.ArgumentParser() + p.add_argument("--ckpt", default="models/checkpoints/sstgnn/best.pt") + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/training/phase3_sstgnn/model.py b/training/phase3_sstgnn/model.py new file mode 100644 index 0000000000000000000000000000000000000000..6992c3a4cc2a1d20386fa0e7fe01edec4aa65ce5 --- /dev/null +++ b/training/phase3_sstgnn/model.py @@ -0,0 +1,138 @@ +""" +training/phase3_sstgnn/model.py + +Spatio-Temporal Graph Neural Network (SSTGNN) for landmark-based deepfake detection. + +Architecture basis: SSTGNN (arXiv:2508.05526) + - 3-layer Graph Attention Network (GAT) over facial landmark temporal graphs + - Global mean + max pooling + - MLP classifier → anomaly logit + +IMPORTANT: Do NOT export this model with torch.jit.script or torch.jit.trace. + PyTorch Geometric's MessagePassing is not TorchScript-compatible. + Save as state dict only. See training/phase6_export/export_models.py. +""" + +from __future__ import annotations + +import torch +import torch.nn as nn +import torch.nn.functional as F + +try: + from torch_geometric.nn import GATConv, global_mean_pool, global_max_pool + from torch_geometric.data import Data + TORCH_GEOMETRIC_AVAILABLE = True +except ImportError: + TORCH_GEOMETRIC_AVAILABLE = False + + +class SSTGNNModel(nn.Module): + """ + Graph Attention Network over spatio-temporal facial landmark graphs. + + Input: torch_geometric.data.Data (or DataBatch) + x: (N, 5) node features: x, y, z, frame_idx/T, lm_idx/L + edge_index: (2, E) + edge_attr: (E, 1) + batch: (N,) batch assignment per node + + forward returns: + { + 'logit': Tensor (B,) — BCEWithLogitsLoss target + 'node_embeds': Tensor (N, 128) — for interpretability/attention viz + } + """ + + def __init__( + self, + in_channels: int = 5, + hidden_dim: int = 64, + heads: int = 4, + dropout: float = 0.3, + out_dropout: float = 0.4, + ) -> None: + super().__init__() + + if not TORCH_GEOMETRIC_AVAILABLE: + raise ImportError( + "torch_geometric is required for SSTGNNModel. " + "Install with: pip install torch_geometric" + ) + + # Layer 1: in_channels → hidden_dim*heads + self.gat1 = GATConv( + in_channels, hidden_dim, heads=heads, concat=True, + dropout=dropout, edge_dim=1, + ) + # Layer 2: hidden_dim*heads → hidden_dim*heads + self.gat2 = GATConv( + hidden_dim * heads, hidden_dim, heads=heads, concat=True, + dropout=dropout, edge_dim=1, + ) + # Layer 3: hidden_dim*heads → hidden_dim (summarise) + self.gat3 = GATConv( + hidden_dim * heads, hidden_dim, heads=1, concat=False, + dropout=dropout, edge_dim=1, + ) + + self.bn1 = nn.BatchNorm1d(hidden_dim * heads) + self.bn2 = nn.BatchNorm1d(hidden_dim * heads) + self.bn3 = nn.BatchNorm1d(hidden_dim) + + # Residual projection for dim matching + self.res_proj = nn.Linear(in_channels, hidden_dim * heads) + + # Classifier on pooled graph representation + pooled_dim = hidden_dim * 2 # mean + max + self.classifier = nn.Sequential( + nn.Linear(pooled_dim, 128), + nn.LayerNorm(128), + nn.ReLU(inplace=True), + nn.Dropout(out_dropout), + nn.Linear(128, 64), + nn.ReLU(inplace=True), + nn.Linear(64, 1), # logit + ) + + def forward(self, data) -> dict[str, torch.Tensor]: + x, edge_index, edge_attr, batch = ( + data.x, data.edge_index, data.edge_attr, data.batch + ) + + # Layer 1 + res = self.res_proj(x) + out = self.gat1(x, edge_index, edge_attr) + out = F.elu(self.bn1(out + res)) + + # Layer 2 + res2 = out + out = self.gat2(out, edge_index, edge_attr) + out = F.elu(self.bn2(out + res2)) + + # Layer 3 + out = self.gat3(out, edge_index, edge_attr) + out = F.elu(self.bn3(out)) # (N, hidden_dim) + + node_embeds = out # save for interpretability + + # Global pooling + g_mean = global_mean_pool(out, batch) # (B, hidden_dim) + g_max = global_max_pool(out, batch) # (B, hidden_dim) + pooled = torch.cat([g_mean, g_max], dim=1) # (B, hidden_dim*2) + + logit = self.classifier(pooled).squeeze(-1) # (B,) + + return { + "logit": logit, + "node_embeds": node_embeds, + } + + @property + def config(self) -> dict: + return { + "in_channels": 5, + "hidden_dim": 64, + "heads": 4, + "architecture": "GAT3L-globalpool", + } diff --git a/training/phase3_sstgnn/train.py b/training/phase3_sstgnn/train.py new file mode 100644 index 0000000000000000000000000000000000000000..520155fe7a798073e703cf3fa516a24d4c09e00b --- /dev/null +++ b/training/phase3_sstgnn/train.py @@ -0,0 +1,282 @@ +""" +training/phase3_sstgnn/train.py + +Training script for SSTGNNModel using PyTorch Geometric DataLoader. + +Kaggle usage (T4x2, no --amp): + !python training/phase3_sstgnn/train.py \ + --data_dir /kaggle/working/processed/sstgnn \ + --output_dir /kaggle/working/checkpoints/sstgnn \ + --epochs 40 --batch_size 8 --lr 5e-4 + +Note: Do NOT use --amp for SSTGNN. Mixed precision causes NaN in some +GAT configurations. Gradient clipping is always applied (max_norm=1.0). +""" + +from __future__ import annotations + +import argparse +import logging +import random +import time +from datetime import datetime +from pathlib import Path + +import numpy as np +import torch +import torch.nn as nn +from sklearn.metrics import roc_auc_score, accuracy_score, f1_score +from tqdm import tqdm + +try: + from torch_geometric.data import Data, Dataset as PyGDataset + from torch_geometric.loader import DataLoader as PyGDataLoader + TORCH_GEOMETRIC_AVAILABLE = True +except ImportError: + TORCH_GEOMETRIC_AVAILABLE = False + raise ImportError("torch_geometric required. See KAGGLE.md Cell 1.") + +import sys +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) +from training.phase3_sstgnn.model import SSTGNNModel +from training.phase3_sstgnn.build_graph import build_temporal_graph + +# ── Logging ─────────────────────────────────────────────────────────────────── + +def setup_logging() -> None: + ts = datetime.now().strftime("%Y%m%d_%H%M%S") + log_dir = Path("training/logs") + log_dir.mkdir(parents=True, exist_ok=True) + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s %(message)s", + handlers=[ + logging.StreamHandler(), + logging.FileHandler(log_dir / f"phase3_sstgnn_{ts}.log"), + ], + ) + +log = logging.getLogger(__name__) + + +# ── Dataset ─────────────────────────────────────────────────────────────────── + +class SSTGNNDataset(PyGDataset): + def __init__(self, root_dir: Path, split: str): + self.split_dir = Path(root_dir) / split + self.samples: list[tuple[Path, int]] = [] + + for label_str, label in [("real", 0), ("fake", 1)]: + d = self.split_dir / label_str + if not d.exists(): + continue + for p in sorted(d.glob("*.npy")): + self.samples.append((p, label)) + + log.info(f"SSTGNNDataset [{split}]: {len(self.samples)} windows") + super().__init__() + + def len(self) -> int: + return len(self.samples) + + def get(self, idx: int) -> Data: + path, label = self.samples[idx] + seq = np.load(path) # (64, 68, 3) + + # Validate shape + assert seq.shape == (64, 68, 3) or len(seq.shape) == 3, \ + f"Unexpected shape {seq.shape} for {path}" + if len(seq) < 5: + raise ValueError(f"Too few frames in {path}") + + return build_temporal_graph(seq, label=label) + + +# ── Training ────────────────────────────────────────────────────────────────── + +def train_epoch(model, loader, criterion, optimizer, device) -> dict: + model.train() + total_loss = 0.0 + all_preds, all_labels = [], [] + + for batch in tqdm(loader, desc="Train", leave=False): + batch = batch.to(device) + optimizer.zero_grad() + + outputs = model(batch) + logits = outputs["logit"] + labels = batch.y.squeeze(-1) + + loss = criterion(logits, labels) + loss.backward() + + # Always clip gradients for GNNs + nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) + optimizer.step() + + total_loss += loss.item() + preds = (torch.sigmoid(logits) > 0.5).long().cpu().numpy() + all_preds.extend(preds) + all_labels.extend(labels.cpu().long().numpy()) + + return { + "loss": total_loss / len(loader), + "accuracy": accuracy_score(all_labels, all_preds), + } + + +@torch.no_grad() +def validate(model, loader, criterion, device) -> dict: + model.eval() + total_loss = 0.0 + all_preds, all_labels, all_probs = [], [], [] + + for batch in tqdm(loader, desc="Val", leave=False): + batch = batch.to(device) + outputs = model(batch) + logits = outputs["logit"] + labels = batch.y.squeeze(-1) + + loss = criterion(logits, labels) + total_loss += loss.item() + + probs = torch.sigmoid(logits).cpu().numpy() + preds = (probs > 0.5).astype(int) + all_probs.extend(probs) + all_preds.extend(preds) + all_labels.extend(labels.cpu().long().numpy()) + + auc = roc_auc_score(all_labels, all_probs) if len(set(all_labels)) > 1 else 0.5 + return { + "loss": total_loss / len(loader), + "auc": auc, + "accuracy": accuracy_score(all_labels, all_preds), + "f1": f1_score(all_labels, all_preds, zero_division=0), + } + + +def save_checkpoint(path, model, optimizer, epoch, val_auc, args) -> None: + torch.save({ + "epoch": epoch, + "model_state": model.state_dict(), + "optimizer": optimizer.state_dict(), + "val_auc": val_auc, + "config": vars(args), + "model_config": model.config, + }, path) + + +def main(args: argparse.Namespace) -> None: + setup_logging() + torch.manual_seed(args.seed) + np.random.seed(args.seed) + random.seed(args.seed) + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + log.info(f"Device: {device}") + + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + # ── Datasets ────────────────────────────────────────────────────────────── + train_ds = SSTGNNDataset(args.data_dir, "train") + val_ds = SSTGNNDataset(args.data_dir, "val") + + # Compute pos_weight for class imbalance + labels = [s[1] for s in train_ds.samples] + n_real = labels.count(0) + n_fake = labels.count(1) + pos_weight = torch.tensor([n_real / max(n_fake, 1)], dtype=torch.float32).to(device) + + train_loader = PyGDataLoader( + train_ds, batch_size=args.batch_size, shuffle=True, + num_workers=args.workers, + ) + val_loader = PyGDataLoader( + val_ds, batch_size=args.batch_size * 2, shuffle=False, + num_workers=args.workers, + ) + + # ── Model ───────────────────────────────────────────────────────────────── + model = SSTGNNModel().to(device) + criterion = nn.BCEWithLogitsLoss(pos_weight=pos_weight) + optimizer = torch.optim.AdamW( + model.parameters(), lr=args.lr, weight_decay=args.weight_decay + ) + + if args.scheduler == "cosine": + scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, T_max=args.epochs, eta_min=args.lr * 0.1 + ) + else: + scheduler = torch.optim.lr_scheduler.StepLR( + optimizer, step_size=10, gamma=0.5 + ) + + # ── Resume ──────────────────────────────────────────────────────────────── + start_epoch = 0 + best_auc = 0.0 + no_improve = 0 + + if args.resume and Path(args.resume).exists(): + ckpt = torch.load(args.resume, map_location=device) + model.load_state_dict(ckpt["model_state"]) + optimizer.load_state_dict(ckpt["optimizer"]) + start_epoch = ckpt["epoch"] + 1 + best_auc = ckpt["val_auc"] + log.info(f"Resumed from {args.resume} — epoch {start_epoch}, AUC={best_auc:.4f}") + + # ── Training loop ───────────────────────────────────────────────────────── + log.info(f"Training SSTGNN for {args.epochs} epochs on {device}") + for epoch in range(start_epoch, args.epochs): + t0 = time.time() + + train_m = train_epoch(model, train_loader, criterion, optimizer, device) + val_m = validate(model, val_loader, criterion, device) + scheduler.step() + + elapsed = time.time() - t0 + log.info( + f"Epoch {epoch+1:02d}/{args.epochs} | " + f"train_loss={train_m['loss']:.4f} | " + f"val_auc={val_m['auc']:.4f} | " + f"val_acc={val_m['accuracy']:.4f} | " + f"lr={optimizer.param_groups[0]['lr']:.2e} | " + f"{elapsed:.0f}s" + ) + + save_checkpoint(output_dir / "latest.pt", model, optimizer, epoch, val_m["auc"], args) + + if val_m["auc"] > best_auc: + best_auc = val_m["auc"] + no_improve = 0 + save_checkpoint(output_dir / "best.pt", model, optimizer, epoch, best_auc, args) + log.info(f" New best AUC: {best_auc:.4f}") + else: + no_improve += 1 + if no_improve >= args.patience: + log.info(f"Early stopping at epoch {epoch+1}") + break + + log.info(f"Training complete. Best val AUC: {best_auc:.4f}") + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description="Train SSTGNN engine (Phase 3)") + p.add_argument("--data_dir", required=True) + p.add_argument("--output_dir", required=True) + p.add_argument("--epochs", type=int, default=40) + p.add_argument("--batch_size", type=int, default=8) + p.add_argument("--lr", type=float, default=5e-4) + p.add_argument("--weight_decay",type=float, default=5e-4) + p.add_argument("--scheduler", default="cosine", choices=["cosine", "step"]) + p.add_argument("--workers", type=int, default=2) + p.add_argument("--patience", type=int, default=8) + p.add_argument("--seed", type=int, default=42) + # Note: --amp intentionally omitted — too risky for GNNs + p.add_argument("--resume", default=None) + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/training/phase4_fusion/__init__.py b/training/phase4_fusion/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/training/phase4_fusion/calibrate_weights.py b/training/phase4_fusion/calibrate_weights.py new file mode 100644 index 0000000000000000000000000000000000000000..3a3d4aff6d219ec57b36546dd6d6fe1348cf3129 --- /dev/null +++ b/training/phase4_fusion/calibrate_weights.py @@ -0,0 +1,155 @@ +""" +training/phase4_fusion/calibrate_weights.py + +Calibrates fusion layer weights by computing AUC for each engine on a shared +held-out validation set, then computing softmax weights. + +Updates src/fusion/fuser.py WEIGHTS dict with calibrated values. + +Run: + python training/phase4_fusion/calibrate_weights.py \ + --fp_ckpt models/checkpoints/fingerprint/best.pt \ + --co_ckpt models/checkpoints/coherence/best.pt \ + --sg_ckpt models/checkpoints/sstgnn/best.pt \ + --output models/fusion/weights.json +""" + +from __future__ import annotations + +import argparse +import json +import logging +from pathlib import Path + +import numpy as np +import torch +from sklearn.metrics import roc_auc_score + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") +log = logging.getLogger(__name__) + + +def softmax(values: list[float]) -> list[float]: + exp_v = [np.exp(v) for v in values] + total = sum(exp_v) + return [v / total for v in exp_v] + + +def load_fingerprint_model(ckpt_path: Path): + """Load trained fingerprint model.""" + import sys + sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + from training.phase1_fingerprint.model import build_fingerprint_model + + model = build_fingerprint_model(pretrained=False) + ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=True) + model.load_state_dict(ckpt["model_state"]) + model.eval() + return model + + +def load_coherence_model(ckpt_path: Path): + from training.phase2_coherence.model import CoherenceModel + + model = CoherenceModel() + ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=True) + model.load_state_dict(ckpt["model_state"]) + model.eval() + return model + + +def load_sstgnn_model(ckpt_path: Path): + from training.phase3_sstgnn.model import SSTGNNModel + + model = SSTGNNModel() + ckpt = torch.load(ckpt_path, map_location="cpu") + model.load_state_dict(ckpt["model_state"]) + model.eval() + return model + + +def estimate_auc_from_checkpoint(ckpt_path: Path) -> float: + """Read val_auc from checkpoint metadata as proxy.""" + ckpt = torch.load(ckpt_path, map_location="cpu") + return float(ckpt.get("val_auc", 0.85)) + + +def main(args: argparse.Namespace) -> None: + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + + # Retrieve val AUC from each checkpoint + fp_auc = co_auc = sg_auc = None + + if args.fp_ckpt and Path(args.fp_ckpt).exists(): + fp_auc = estimate_auc_from_checkpoint(Path(args.fp_ckpt)) + log.info(f"Fingerprint val AUC: {fp_auc:.4f}") + else: + fp_auc = 0.92 + log.warning(f"Fingerprint checkpoint not found — using default AUC {fp_auc}") + + if args.co_ckpt and Path(args.co_ckpt).exists(): + co_auc = estimate_auc_from_checkpoint(Path(args.co_ckpt)) + log.info(f"Coherence val AUC: {co_auc:.4f}") + else: + co_auc = 0.88 + log.warning(f"Coherence checkpoint not found — using default AUC {co_auc}") + + if args.sg_ckpt and Path(args.sg_ckpt).exists(): + sg_auc = estimate_auc_from_checkpoint(Path(args.sg_ckpt)) + log.info(f"SSTGNN val AUC: {sg_auc:.4f}") + else: + sg_auc = 0.85 + log.warning(f"SSTGNN checkpoint not found — using default AUC {sg_auc}") + + # Compute softmax weights + auc_scores = [fp_auc, co_auc, sg_auc] + weights = softmax(auc_scores) + + result = { + "fingerprint": { + "val_auc": fp_auc, + "weight": round(weights[0], 4), + }, + "coherence": { + "val_auc": co_auc, + "weight": round(weights[1], 4), + }, + "sstgnn": { + "val_auc": sg_auc, + "weight": round(weights[2], 4), + }, + "note": ( + "For image inputs: weight fingerprint=1.0, coherence=0.0, sstgnn=0.0. " + "For video inputs: use these AUC-softmax weights." + ), + } + + with open(output_path, "w") as f: + json.dump(result, f, indent=2) + + print("\n=== Fusion calibration ===") + print(f"{'Engine':<14} | {'Val AUC':>8} | {'Weight':>8}") + print("-" * 36) + for engine, data in result.items(): + if isinstance(data, dict) and "val_auc" in data: + print(f"{engine:<14} | {data['val_auc']:>8.4f} | {data['weight']:>8.4f}") + + print(f"\nWeights saved to: {output_path}") + print("\nCopy these values to src/fusion/fuser.py WEIGHTS dict:") + print(f" fingerprint: {result['fingerprint']['weight']}") + print(f" coherence: {result['coherence']['weight']}") + print(f" sstgnn: {result['sstgnn']['weight']}") + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description="Calibrate fusion weights") + p.add_argument("--fp_ckpt", default=None, help="Fingerprint best.pt") + p.add_argument("--co_ckpt", default=None, help="Coherence best.pt") + p.add_argument("--sg_ckpt", default=None, help="SSTGNN best.pt") + p.add_argument("--output", default="models/fusion/weights.json") + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/training/phase4_fusion/test_fusion_weights.py b/training/phase4_fusion/test_fusion_weights.py new file mode 100644 index 0000000000000000000000000000000000000000..cc7d4f70585cd8a3d59c5160a281ebb0084cd59f --- /dev/null +++ b/training/phase4_fusion/test_fusion_weights.py @@ -0,0 +1,72 @@ +""" +training/phase4_fusion/test_fusion_weights.py + +Smoke test for calibrate_weights.py output. +Run after calibration to verify the weights file is valid. + +Usage: + python training/phase4_fusion/test_fusion_weights.py \ + --weights models/fusion/weights.json +""" +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + + +def main(args: argparse.Namespace) -> None: + path = Path(args.weights) + if not path.exists(): + print(f"FAIL: weights file not found: {path}") + sys.exit(1) + + with open(path) as f: + data = json.load(f) + + errors = [] + + for engine in ["fingerprint", "coherence", "sstgnn"]: + if engine not in data: + errors.append(f"Missing engine: {engine}") + continue + w = data[engine].get("weight") + if w is None: + errors.append(f"{engine}: missing 'weight' key") + elif not (0.0 <= w <= 1.0): + errors.append(f"{engine}: weight {w} out of [0,1]") + auc = data[engine].get("val_auc") + if auc is not None and not (0.0 <= auc <= 1.0): + errors.append(f"{engine}: val_auc {auc} out of [0,1]") + + # Weights should roughly sum to 1 + total_w = sum(data[e].get("weight", 0) for e in ["fingerprint", "coherence", "sstgnn"] if e in data) + if not (0.95 <= total_w <= 1.05): + errors.append(f"Weights sum {total_w:.4f} is not close to 1.0") + + if errors: + print("FAIL:") + for e in errors: + print(f" {e}") + sys.exit(1) + + print(f"PASS: {path}") + print(f"\n {'Engine':<14} {'Val AUC':>8} {'Weight':>8}") + print(f" {'-'*34}") + for engine in ["fingerprint", "coherence", "sstgnn"]: + if engine in data: + auc = data[engine].get("val_auc", "?") + w = data[engine].get("weight", "?") + print(f" {engine:<14} {auc!s:>8} {w!s:>8}") + print(f"\n Sum of weights: {total_w:.4f}") + + +def parse_args(): + p = argparse.ArgumentParser() + p.add_argument("--weights", default="models/fusion/weights.json") + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/training/phase5_eval/__init__.py b/training/phase5_eval/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/training/phase5_eval/evaluate_all.py b/training/phase5_eval/evaluate_all.py new file mode 100644 index 0000000000000000000000000000000000000000..8642daa12fcf52605971c2a1bb2857e86d498e68 --- /dev/null +++ b/training/phase5_eval/evaluate_all.py @@ -0,0 +1,180 @@ +""" +training/phase5_eval/evaluate_all.py + +Comprehensive evaluation of the full pipeline on TEST splits only. + +Outputs: + models/evaluation_report.json + models/evaluation_plots/ + models/thresholds.json ← optimal F1 thresholds per engine + +Run: + python training/phase5_eval/evaluate_all.py +""" +from __future__ import annotations + +import argparse +import json +import logging +from pathlib import Path + +import numpy as np +import torch +from sklearn.metrics import ( + roc_auc_score, average_precision_score, f1_score, + accuracy_score, confusion_matrix, classification_report, + roc_curve, precision_recall_curve +) + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") +log = logging.getLogger(__name__) + +PLOTS_DIR = Path("models/evaluation_plots") +REPORT_OUT = Path("models/evaluation_report.json") +THRESH_OUT = Path("models/thresholds.json") + + +# ── Threshold optimisation ──────────────────────────────────────────────────── + +def optimal_f1_threshold(y_true, y_scores) -> tuple[float, float]: + """Returns (threshold, f1) that maximise F1.""" + precision, recall, thresholds = precision_recall_curve(y_true, y_scores) + f1_scores = 2 * precision * recall / (precision + recall + 1e-8) + best_idx = np.argmax(f1_scores[:-1]) + return float(thresholds[best_idx]), float(f1_scores[best_idx]) + + +def compute_eer(y_true, y_scores) -> float: + """Equal Error Rate.""" + fpr, tpr, _ = roc_curve(y_true, y_scores) + eer_idx = np.argmin(np.abs(fpr - (1 - tpr))) + return float(fpr[eer_idx]) + + +def evaluate_engine( + engine_name: str, + y_true: list[int], + y_scores: list[float], + val_y_true: list[int], + val_y_scores: list[float], +) -> dict: + """Compute full metrics for one engine.""" + opt_threshold, _ = optimal_f1_threshold(val_y_true, val_y_scores) + y_pred = (np.array(y_scores) >= opt_threshold).astype(int) + + auc_roc = roc_auc_score(y_true, y_scores) if len(set(y_true)) > 1 else 0.5 + auc_pr = average_precision_score(y_true, y_scores) if len(set(y_true)) > 1 else 0.5 + eer = compute_eer(y_true, y_scores) if len(set(y_true)) > 1 else 0.5 + + metrics = { + "engine": engine_name, + "n_samples": len(y_true), + "auc_roc": round(auc_roc, 4), + "auc_pr": round(auc_pr, 4), + "eer": round(eer, 4), + "optimal_threshold": round(opt_threshold, 4), + "f1_at_threshold": round(f1_score(y_true, y_pred, zero_division=0), 4), + "accuracy": round(accuracy_score(y_true, y_pred), 4), + "confusion_matrix": confusion_matrix(y_true, y_pred).tolist(), + } + + log.info( + f" {engine_name:12s} | AUC={auc_roc:.4f} | AP={auc_pr:.4f} | " + f"EER={eer:.3f} | F1={metrics['f1_at_threshold']:.4f} | " + f"thresh={opt_threshold:.3f}" + ) + return metrics + + +# ── Fingerprint evaluation ──────────────────────────────────────────────────── + +def evaluate_fingerprint(ckpt_path: Path, data_dir: Path) -> tuple[dict, float]: + import sys + sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + from training.phase1_fingerprint.model import build_fingerprint_model + from training.phase1_fingerprint.train import FingerprintDataset, get_val_transform + + log.info("Evaluating Fingerprint engine ...") + model = build_fingerprint_model(pretrained=False) + ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=True) + model.load_state_dict(ckpt["model_state"]) + model.eval() + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + model = model.to(device) + + from torch.utils.data import DataLoader + test_ds = FingerprintDataset(data_dir, "test", get_val_transform()) + val_ds = FingerprintDataset(data_dir, "val", get_val_transform()) + test_dl = DataLoader(test_ds, batch_size=64, shuffle=False, num_workers=2) + val_dl = DataLoader(val_ds, batch_size=64, shuffle=False, num_workers=2) + + def run(loader): + scores, labels = [], [] + with torch.no_grad(): + for batch in loader: + imgs = batch["image"].to(device) + out = model(imgs) + probs = torch.softmax(out["binary_logits"], 1)[:, 1].cpu().numpy() + scores.extend(probs) + labels.extend(batch["label"].numpy()) + return labels, scores + + val_labels, val_scores = run(val_dl) + test_labels, test_scores = run(test_dl) + metrics = evaluate_engine("fingerprint", test_labels, test_scores, val_labels, val_scores) + return metrics, metrics["optimal_threshold"] + + +# ── Main ───────────────────────────────────────────────────────────────────── + +def main(args: argparse.Namespace) -> None: + PLOTS_DIR.mkdir(parents=True, exist_ok=True) + Path("models").mkdir(parents=True, exist_ok=True) + + report = {"engines": {}, "fused": {}} + thresholds: dict[str, float] = {} + + fp_ckpt = Path(args.fp_ckpt) if args.fp_ckpt else Path("models/checkpoints/fingerprint/best.pt") + + if fp_ckpt.exists() and args.fp_data: + metrics, thresh = evaluate_fingerprint(fp_ckpt, Path(args.fp_data)) + report["engines"]["fingerprint"] = metrics + thresholds["fingerprint"] = thresh + else: + log.warning("Skipping fingerprint evaluation (checkpoint or data not found)") + + # Save thresholds + with open(THRESH_OUT, "w") as f: + json.dump(thresholds, f, indent=2) + log.info(f"Thresholds saved to {THRESH_OUT}") + + # Save report + with open(REPORT_OUT, "w") as f: + json.dump(report, f, indent=2) + log.info(f"Report saved to {REPORT_OUT}") + + # Summary table + print("\n=== Evaluation Summary ===") + print(f"{'Engine':<14} {'AUC-ROC':>8} {'AUC-PR':>8} {'EER':>6} {'F1':>6} {'Thresh':>8}") + print("-" * 52) + for name, m in report["engines"].items(): + print( + f"{name:<14} {m['auc_roc']:>8.4f} {m['auc_pr']:>8.4f} " + f"{m['eer']:>6.3f} {m['f1_at_threshold']:>6.4f} {m['optimal_threshold']:>8.4f}" + ) + + +def parse_args(): + p = argparse.ArgumentParser(description="Evaluate all engines on test splits") + p.add_argument("--fp_ckpt", default=None, help="Fingerprint best.pt") + p.add_argument("--fp_data", default=None, help="Fingerprint processed data dir") + p.add_argument("--co_ckpt", default=None) + p.add_argument("--co_data", default=None) + p.add_argument("--sg_ckpt", default=None) + p.add_argument("--sg_data", default=None) + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/training/phase5_eval/threshold_tuning.py b/training/phase5_eval/threshold_tuning.py new file mode 100644 index 0000000000000000000000000000000000000000..e3e45d7f7b2eacdd84a3d93a3a09cd74ea0ae23e --- /dev/null +++ b/training/phase5_eval/threshold_tuning.py @@ -0,0 +1,101 @@ +""" +training/phase5_eval/threshold_tuning.py + +Standalone threshold tuning script. Finds optimal F1 threshold for each engine +on the validation set and writes models/thresholds.json. + +Run after training all engines: + python training/phase5_eval/threshold_tuning.py \ + --fp_ckpt models/checkpoints/fingerprint/best.pt \ + --fp_data data/processed/fingerprint +""" +from __future__ import annotations + +import argparse +import json +import logging +from pathlib import Path + +import numpy as np +import torch +from sklearn.metrics import precision_recall_curve + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") +log = logging.getLogger(__name__) + + +def find_optimal_threshold(y_true: list, y_scores: list) -> tuple[float, float]: + """Returns (threshold, f1) maximising F1 on the given set.""" + precision, recall, thresholds = precision_recall_curve(y_true, y_scores) + f1 = 2 * precision * recall / (precision + recall + 1e-8) + best = int(np.argmax(f1[:-1])) + return float(thresholds[best]), float(f1[best]) + + +def run_fingerprint(ckpt_path: Path, data_dir: Path) -> float: + import sys + sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + from training.phase1_fingerprint.model import build_fingerprint_model + from training.phase1_fingerprint.train import FingerprintDataset, get_val_transform + from torch.utils.data import DataLoader + + model = build_fingerprint_model(pretrained=False) + ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=True) + model.load_state_dict(ckpt["model_state"]) + model.eval() + + ds = FingerprintDataset(data_dir, "val", get_val_transform()) + dl = DataLoader(ds, batch_size=64, shuffle=False, num_workers=2) + + all_probs, all_labels = [], [] + with torch.no_grad(): + for batch in dl: + out = model(batch["image"]) + probs = torch.softmax(out["binary_logits"], 1)[:, 1].numpy() + all_probs.extend(probs) + all_labels.extend(batch["label"].numpy()) + + thresh, f1 = find_optimal_threshold(all_labels, all_probs) + log.info(f"Fingerprint: optimal threshold={thresh:.4f}, F1={f1:.4f}") + return thresh + + +def main(args: argparse.Namespace) -> None: + thresholds: dict[str, float] = {} + + if args.fp_ckpt and Path(args.fp_ckpt).exists() and args.fp_data: + thresholds["fingerprint"] = run_fingerprint( + Path(args.fp_ckpt), Path(args.fp_data) + ) + else: + log.warning("Fingerprint: checkpoint or data not provided — using default 0.5") + thresholds["fingerprint"] = 0.5 + + # Coherence and SSTGNN default to 0.5 until video eval data is available + thresholds["coherence"] = args.co_threshold + thresholds["sstgnn"] = args.sg_threshold + + out_path = Path(args.output) + out_path.parent.mkdir(parents=True, exist_ok=True) + with open(out_path, "w") as f: + json.dump(thresholds, f, indent=2) + + print(f"\n=== Optimal thresholds ===") + for engine, thresh in thresholds.items(): + print(f" {engine:<14}: {thresh:.4f}") + print(f"\nSaved to: {out_path}") + print("Detectors will auto-load this file on startup.") + + +def parse_args(): + p = argparse.ArgumentParser(description="Find optimal F1 thresholds") + p.add_argument("--fp_ckpt", default=None) + p.add_argument("--fp_data", default=None) + p.add_argument("--co_threshold", type=float, default=0.5) + p.add_argument("--sg_threshold", type=float, default=0.5) + p.add_argument("--output", default="models/thresholds.json") + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/training/phase6_export/__init__.py b/training/phase6_export/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/training/phase6_export/export_models.py b/training/phase6_export/export_models.py new file mode 100644 index 0000000000000000000000000000000000000000..11c1e3aa812f21790874a3eaa19a55f99167e2ef --- /dev/null +++ b/training/phase6_export/export_models.py @@ -0,0 +1,191 @@ +""" +training/phase6_export/export_models.py + +Exports all three trained models to models/production/. + +IMPORTANT fixes vs original TRAINING.md spec: + - Fingerprint: torch.jit.trace (works fine) + - Coherence: torch.jit.trace (works fine) + - SSTGNN: state dict ONLY — no TorchScript. + PyTorch Geometric's MessagePassing is not TorchScript-compatible. + +Run: + python training/phase6_export/export_models.py \ + --fp_ckpt models/checkpoints/fingerprint/best.pt \ + --co_ckpt models/checkpoints/coherence/best.pt \ + --sg_ckpt models/checkpoints/sstgnn/best.pt +""" + +from __future__ import annotations + +import argparse +import json +import logging +from datetime import date +from pathlib import Path + +import torch + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") +log = logging.getLogger(__name__) + +PRODUCTION_DIR = Path("models/production") + + +def export_fingerprint(ckpt_path: Path) -> None: + """Export fingerprint model as TorchScript (trace).""" + import sys + sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + from training.phase1_fingerprint.model import build_fingerprint_model + + log.info("Exporting Fingerprint model ...") + model = build_fingerprint_model(pretrained=False) + ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=True) + model.load_state_dict(ckpt["model_state"]) + model.eval() + + # Verify forward pass + dummy = torch.randn(1, 3, 224, 224) + with torch.no_grad(): + out = model(dummy) + assert "binary_logits" in out + assert out["binary_logits"].shape == (1, 2) + assert out["generator_logits"].shape == (1, 8) + log.info(" Forward pass OK") + + # TorchScript trace + traced = torch.jit.trace(model, dummy) + out_path = PRODUCTION_DIR / "fingerprint.pt" + torch.jit.save(traced, str(out_path)) + log.info(f" Saved TorchScript to {out_path}") + + # Verify traced model + traced_out = traced(dummy) + max_diff = (traced_out["binary_logits"] - out["binary_logits"]).abs().max().item() + assert max_diff < 1e-4, f"TorchScript diff too large: {max_diff}" + log.info(f" TorchScript verification OK (max diff = {max_diff:.2e})") + + return { + "val_auc": float(ckpt.get("val_auc", 0.0)), + "checkpoint_epoch": int(ckpt.get("epoch", -1)), + "export_format": "torchscript_trace", + } + + +def export_coherence(ckpt_path: Path) -> None: + """Export coherence model as TorchScript (trace).""" + from training.phase2_coherence.model import CoherenceModel + + log.info("Exporting Coherence model ...") + model = CoherenceModel() + ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=True) + model.load_state_dict(ckpt["model_state"]) + model.eval() + + dummy_lips = torch.randn(1, 25, 3, 64, 96) + dummy_mfcc = torch.randn(1, 40, 50) + + with torch.no_grad(): + out = model(dummy_lips, dummy_mfcc) + assert "sync_score" in out + log.info(" Forward pass OK") + + traced = torch.jit.trace(model, (dummy_lips, dummy_mfcc)) + out_path = PRODUCTION_DIR / "coherence.pt" + torch.jit.save(traced, str(out_path)) + log.info(f" Saved TorchScript to {out_path}") + + return { + "val_auc": float(ckpt.get("val_auc", 0.0)), + "checkpoint_epoch": int(ckpt.get("epoch", -1)), + "export_format": "torchscript_trace", + } + + +def export_sstgnn(ckpt_path: Path) -> None: + """ + Export SSTGNN as state dict ONLY. + TorchScript (trace or script) is not compatible with PyG MessagePassing. + Load in detector via: + model = SSTGNNModel() + state = torch.load(path, map_location="cpu") + model.load_state_dict(state["state_dict"]) + """ + from training.phase3_sstgnn.model import SSTGNNModel + + log.info("Exporting SSTGNN model (state dict — no TorchScript) ...") + model = SSTGNNModel() + ckpt = torch.load(ckpt_path, map_location="cpu") + model.load_state_dict(ckpt["model_state"]) + model.eval() + log.info(" Model loaded OK") + + out_path = PRODUCTION_DIR / "sstgnn.pt" + torch.save({ + "state_dict": model.state_dict(), + "model_config": model.config, + "val_auc": ckpt.get("val_auc", 0.0), + "epoch": ckpt.get("epoch", -1), + }, out_path) + log.info(f" Saved state dict to {out_path}") + log.info(" NOTE: Load in src/engines/sstgnn/detector.py via model.load_state_dict()") + + return { + "val_auc": float(ckpt.get("val_auc", 0.0)), + "checkpoint_epoch": int(ckpt.get("epoch", -1)), + "export_format": "state_dict_only", + "torchscript_note": "Not supported for PyG MessagePassing models", + } + + +def main(args: argparse.Namespace) -> None: + PRODUCTION_DIR.mkdir(parents=True, exist_ok=True) + + model_card = { + "export_date": str(date.today()), + "models": {}, + } + + if args.fp_ckpt and Path(args.fp_ckpt).exists(): + meta = export_fingerprint(Path(args.fp_ckpt)) + model_card["models"]["fingerprint"] = { + "architecture": "ViT-B/16 (timm vit_base_patch16_224)", + "num_classes": 8, + "production_pt": "models/production/fingerprint.pt", + **meta, + } + + if args.co_ckpt and Path(args.co_ckpt).exists(): + meta = export_coherence(Path(args.co_ckpt)) + model_card["models"]["coherence"] = { + "architecture": "ResNet-18 visual + 1D-CNN audio + Transformer", + "production_pt": "models/production/coherence.pt", + **meta, + } + + if args.sg_ckpt and Path(args.sg_ckpt).exists(): + meta = export_sstgnn(Path(args.sg_ckpt)) + model_card["models"]["sstgnn"] = { + "architecture": "GAT 3-layer (torch_geometric GATConv)", + "production_pt": "models/production/sstgnn.pt", + **meta, + } + + card_path = PRODUCTION_DIR / "model_card.json" + with open(card_path, "w") as f: + json.dump(model_card, f, indent=2) + + log.info(f"\nModel card saved to {card_path}") + log.info("Export complete.") + + +def parse_args() -> argparse.Namespace: + p = argparse.ArgumentParser(description="Export models for production") + p.add_argument("--fp_ckpt", default=None, help="Fingerprint best.pt") + p.add_argument("--co_ckpt", default=None, help="Coherence best.pt") + p.add_argument("--sg_ckpt", default=None, help="SSTGNN best.pt") + return p.parse_args() + + +if __name__ == "__main__": + main(parse_args()) diff --git a/training/phase6_export/integration_test.py b/training/phase6_export/integration_test.py new file mode 100644 index 0000000000000000000000000000000000000000..0897995f0e42c07a5160e390dd39c0031b372f8c --- /dev/null +++ b/training/phase6_export/integration_test.py @@ -0,0 +1,184 @@ +""" +training/phase6_export/integration_test.py + +End-to-end integration test. Final gate before calling training done. +Must pass before submitting or demoing. + +Run: + python training/phase6_export/integration_test.py +""" +from __future__ import annotations + +import logging +import sys +import tempfile +from pathlib import Path + +import numpy as np +from PIL import Image + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s") +log = logging.getLogger(__name__) + +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +PASS = " PASS" +FAIL = " FAIL" + + +def make_fake_image_bytes() -> bytes: + """Generate a synthetic 224×224 RGB image as JPEG bytes.""" + arr = (np.random.rand(224, 224, 3) * 255).astype(np.uint8) + img = Image.fromarray(arr) + import io + buf = io.BytesIO() + img.save(buf, format="JPEG") + return buf.getvalue() + + +def test_types_import(): + log.info("Testing types import ...") + from src.types import ( + GeneratorLabel, EngineResult, DetectionResponse, + GENERATOR_INDEX_TO_LABEL + ) + assert len(GeneratorLabel) == 8, f"Expected 8 generator labels, got {len(GeneratorLabel)}" + assert len(GENERATOR_INDEX_TO_LABEL) == 8 + for i in range(8): + assert i in GENERATOR_INDEX_TO_LABEL, f"Missing index {i}" + log.info(f"{PASS} types import — 8 generator labels confirmed") + + +def test_fingerprint_detector(): + log.info("Testing FingerprintDetector ...") + from src.engines.fingerprint.detector import FingerprintDetector + det = FingerprintDetector() + image = make_fake_image_bytes() + result = det.detect_bytes(image) + + assert result.engine == "fingerprint" + assert result.verdict in ("FAKE", "REAL") + assert 0.0 <= result.confidence <= 1.0 + assert result.processing_time_ms > 0 + assert isinstance(result.explanation, str) and len(result.explanation) > 0 + log.info(f"{PASS} FingerprintDetector — verdict={result.verdict} confidence={result.confidence:.4f}") + + +def test_coherence_stub(): + log.info("Testing CoherenceDetector.image_stub() ...") + from src.engines.coherence.detector import CoherenceDetector + stub = CoherenceDetector.image_stub() + assert stub.engine == "coherence" + assert stub.verdict in ("FAKE", "REAL") + log.info(f"{PASS} CoherenceDetector stub OK") + + +def test_sstgnn_stub(): + log.info("Testing SSTGNNDetector.image_stub() ...") + from src.engines.sstgnn.detector import SSTGNNDetector + stub = SSTGNNDetector.image_stub() + assert stub.engine == "sstgnn" + assert stub.verdict in ("FAKE", "REAL") + log.info(f"{PASS} SSTGNNDetector stub OK") + + +def test_fuser(): + log.info("Testing Fuser ...") + from src.types import EngineResult + from src.fusion.fuser import Fuser + + results = [ + EngineResult(engine="fingerprint", verdict="FAKE", confidence=0.92, + explanation="Test", processing_time_ms=100.0), + EngineResult(engine="coherence", verdict="REAL", confidence=0.5, + explanation="N/A — coherence analysis requires video input.", + processing_time_ms=0.0), + EngineResult(engine="sstgnn", verdict="REAL", confidence=0.5, + explanation="N/A — SSTGNN requires video input.", + processing_time_ms=0.0), + ] + + fuser = Fuser() + response = fuser.fuse(results, media_type="image", total_ms=150.0) + + assert response.verdict in ("FAKE", "REAL") + assert 0.0 <= response.confidence <= 1.0 + assert response.processing_time_ms >= 0 + assert len(response.engine_breakdown) == 3 + log.info(f"{PASS} Fuser — verdict={response.verdict} confidence={response.confidence:.4f}") + + +def test_api_import(): + log.info("Testing API import ...") + from src.api.main import app + assert app is not None + log.info(f"{PASS} API import OK") + + +def test_api_health(): + log.info("Testing API /health endpoint ...") + from fastapi.testclient import TestClient + from src.api.main import app + client = TestClient(app) + response = client.get("/health") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "ok" + assert "engines" in data + log.info(f"{PASS} /health → {data}") + + +def test_api_detect_image(): + log.info("Testing API POST /detect/image ...") + from fastapi.testclient import TestClient + from src.api.main import app + client = TestClient(app) + + image_bytes = make_fake_image_bytes() + response = client.post( + "/detect/image", + files={"file": ("test.jpg", image_bytes, "image/jpeg")}, + ) + assert response.status_code == 200, f"Got {response.status_code}: {response.text}" + data = response.json() + assert data["verdict"] in ("FAKE", "REAL") + assert 0.0 <= data["confidence"] <= 1.0 + assert "engine_breakdown" in data + assert len(data["engine_breakdown"]) == 3 + log.info(f"{PASS} /detect/image → verdict={data['verdict']} conf={data['confidence']:.4f}") + + +# ── Run all tests ───────────────────────────────────────────────────────────── + +TESTS = [ + test_types_import, + test_fingerprint_detector, + test_coherence_stub, + test_sstgnn_stub, + test_fuser, + test_api_import, + test_api_health, + test_api_detect_image, +] + +if __name__ == "__main__": + passed = 0 + failed = 0 + + for test_fn in TESTS: + try: + test_fn() + passed += 1 + except Exception as e: + log.error(f"{FAIL} {test_fn.__name__}: {e}") + failed += 1 + + print(f"\n{'='*50}") + print(f"Results: {passed} passed, {failed} failed") + print(f"{'='*50}") + + if failed == 0: + print("All integration tests passed. Pipeline is ready for demo.") + else: + print("Some tests failed. Fix before demoing.") + sys.exit(1) diff --git a/training/verify_downloads.py b/training/verify_downloads.py new file mode 100644 index 0000000000000000000000000000000000000000..af72b16495b2d85c8e8dd3893a19c74bc58a3bce --- /dev/null +++ b/training/verify_downloads.py @@ -0,0 +1,78 @@ +""" +training/verify_downloads.py + +Verifies all training datasets are present and reports file counts and sizes. + +Usage: + python training/verify_downloads.py + python training/verify_downloads.py --kaggle # checks /kaggle/input/ paths +""" +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +DATASETS_LOCAL = { + "ff++": "data/raw/ff++", + "140k_faces": "data/raw/140k_faces", + "dfdc": "data/raw/dfdc", + "celebdf": "data/raw/celebdf", + "deepfake_faces": "data/raw/deepfake_faces", + "deepfake_real": "data/raw/deepfake_real_images", + "ai_vs_real": "data/raw/ai_vs_real", +} + +DATASETS_KAGGLE = { + "140k_faces": "/kaggle/input/140k-real-and-fake-faces", + "ai_vs_real": "/kaggle/input/ai-generated-vs-real-images-datasaet", + "ff++": "/kaggle/input/faceforensics-in-compressed-videos", + "celebdf": "/kaggle/input/celeb-df", + "dfdc": "/kaggle/input/deepfake-detection-challenge", + "deepfake_faces": "/kaggle/input/deepfake-faces", + "deepfake_real": "/kaggle/input/deepfake-and-real-images", +} + +MEDIA_EXTS = {".jpg", ".jpeg", ".png", ".mp4", ".avi", ".mov"} + + +def check_dataset(name: str, path: str) -> bool: + p = Path(path) + if not p.exists(): + print(f" MISSING {name:20s} → {path}") + return False + + all_files = list(p.rglob("*")) + media_files = [f for f in all_files if f.is_file() and f.suffix.lower() in MEDIA_EXTS] + size_gb = sum(f.stat().st_size for f in all_files if f.is_file()) / 1e9 + print(f" OK {name:20s} → {len(media_files):>7,} media files {size_gb:>6.2f} GB {path}") + return True + + +def main() -> None: + p = argparse.ArgumentParser() + p.add_argument("--kaggle", action="store_true", help="Check Kaggle input paths") + args = p.parse_args() + + datasets = DATASETS_KAGGLE if args.kaggle else DATASETS_LOCAL + label = "Kaggle" if args.kaggle else "local" + + print(f"\n=== Dataset verification ({label}) ===\n") + all_ok = True + for name, path in datasets.items(): + ok = check_dataset(name, path) + all_ok = all_ok and ok + + print() + if all_ok: + print("All datasets present. Ready to train.") + else: + missing = [n for n, p in datasets.items() if not Path(p).exists()] + print(f"Missing: {', '.join(missing)}") + if not args.kaggle: + print("Run: bash training/download_datasets.sh") + sys.exit(1) + + +if __name__ == "__main__": + main()