e-cagan commited on
Commit
c679d56
·
0 Parent(s):

Deploy AUGUR

Browse files
.dockerignore ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Data & datasets (huge, never needed in image)
2
+ data/
3
+ *.tif
4
+ *.mp4
5
+
6
+ # Training artifacts
7
+ wandb/
8
+ checkpoints/*.pt
9
+ !checkpoints/model.onnx
10
+ !checkpoints/model.onnx.data
11
+
12
+ # Python
13
+ venv/
14
+ __pycache__/
15
+ *.pyc
16
+ .pytest_cache/
17
+
18
+ # Node (frontend build context if shared)
19
+ node_modules/
20
+ frontend/dist/
21
+
22
+ # Git & IDE
23
+ .git/
24
+ .gitignore
25
+ *.md
26
+ .vscode/
27
+ .idea/
28
+
29
+ # Env
30
+ .env
31
+ *.db
.gitattributes ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.onnx.data filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ node_modules/
2
+ __pycache__/
3
+ *.pyc
4
+ venv/
5
+ .env
Dockerfile ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Combined single-image build for deployment (e.g. Hugging Face Spaces).
2
+ # Backend serves BOTH the API and the built frontend (same origin, no CORS,
3
+ # no nginx). One container, runnable with: docker run -p 7860:7860
4
+ #
5
+ # Build from repo root:
6
+ # docker build -f docker/Dockerfile.deploy -t augur-deploy .
7
+
8
+ # ---- Stage 1: build the frontend ----
9
+ FROM node:20-slim AS frontend
10
+
11
+ WORKDIR /app
12
+ COPY frontend/package.json frontend/package-lock.json* ./
13
+ RUN npm install
14
+ COPY frontend/ .
15
+ # Same-origin: the frontend calls /predict directly (no /api proxy here).
16
+ ENV VITE_API_URL=/predict
17
+ RUN npm run build
18
+ # Output: /app/dist
19
+
20
+ # ---- Stage 2: backend + bundled frontend ----
21
+ FROM python:3.10-slim
22
+
23
+ RUN apt-get update && apt-get install -y --no-install-recommends \
24
+ libglib2.0-0 \
25
+ && rm -rf /var/lib/apt/lists/*
26
+
27
+ WORKDIR /app
28
+
29
+ # Python deps (CPU torch wheel, lean serving set)
30
+ COPY requirements-inference.txt .
31
+ RUN pip install --no-cache-dir \
32
+ --extra-index-url https://download.pytorch.org/whl/cpu \
33
+ -r requirements-inference.txt
34
+
35
+ # Backend code + model (onnx + external data)
36
+ COPY src/ ./src/
37
+ COPY backend/ ./backend/
38
+ COPY checkpoints/model.onnx* ./checkpoints/
39
+
40
+ # Built frontend from stage 1 -> served by FastAPI StaticFiles at /
41
+ COPY --from=frontend /app/dist ./static
42
+
43
+ # HF Spaces expects the app on port 7860 by default
44
+ EXPOSE 7860
45
+ CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AUGUR Anomaly Detection
3
+ emoji: 📈
4
+ colorFrom: blue
5
+ colorTo: red
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ license: mit
10
+ ---
11
+
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
backend/main.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI backend for video anomaly detection (M4).
3
+
4
+ Serves the M3 U-Net future-frame predictor (via ONNX) over HTTP.
5
+ Minimal first slice: a single POST /predict endpoint that accepts a video
6
+ upload and returns per-frame anomaly scores as JSON.
7
+
8
+ Run:
9
+ uvicorn backend.main:app --reload --port 8000
10
+
11
+ Then POST a video file to http://localhost:8000/predict (multipart form-data,
12
+ field name "file").
13
+ """
14
+
15
+ import os
16
+ import tempfile
17
+ from contextlib import asynccontextmanager
18
+ import io
19
+ import base64
20
+ import numpy as np
21
+ import matplotlib
22
+ matplotlib.use("Agg") # GUI yok, sadece render — server'da şart
23
+ import matplotlib.cm as cm
24
+
25
+ from fastapi import FastAPI, UploadFile, File, HTTPException
26
+ from fastapi.middleware.cors import CORSMiddleware
27
+ from fastapi.staticfiles import StaticFiles
28
+
29
+ from prometheus_fastapi_instrumentator import Instrumentator
30
+ from prometheus_client import Histogram, Gauge
31
+
32
+ from src.inference.stream import process_frames # reused; see note on video below
33
+ from src.inference.stream import process_video
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Configuration
38
+ # ---------------------------------------------------------------------------
39
+
40
+ ONNX_PATH = "checkpoints/model.onnx"
41
+
42
+ # Anomaly threshold on the raw per-frame MSE score.
43
+ #
44
+ # DECISION POINT (yours): this should be calibrated from the NORMAL training
45
+ # distribution, not guessed. The principled way:
46
+ # 1. Run the stream over the normal training clips.
47
+ # 2. Collect all per-frame scores (these are all "normal" by construction).
48
+ # 3. Set THRESHOLD = mean + k * std (k ~ 2-3), i.e. "how far above normal
49
+ # counts as anomalous".
50
+ # The value below is a placeholder based on the M3 histogram (normal frames
51
+ # clustered ~0.0002-0.0003, anomalies tailing to ~0.0011). Replace it with a
52
+ # calibrated value and document the choice in milestones.md.
53
+ THRESHOLD = 0.000291
54
+
55
+
56
+ def make_overlay_png(frame_img: np.ndarray, heatmap: np.ndarray, alpha: float = 0.5) -> str:
57
+ """
58
+ Overlay the anomaly heatmap on the grayscale frame, return base64 PNG.
59
+
60
+ frame_img: (128,128) preprocessed frame in [-1,1]
61
+ heatmap: (128,128) prediction error (>=0), arbitrary scale
62
+ Returns: base64-encoded PNG string (no data: prefix)
63
+ """
64
+ from PIL import Image
65
+
66
+ # 1. Frame [-1,1] -> [0,1] grayscale, then to RGB
67
+ frame01 = (frame_img + 1.0) / 2.0
68
+ frame01 = np.clip(frame01, 0, 1)
69
+ base_rgb = np.stack([frame01] * 3, axis=-1) # (128,128,3)
70
+
71
+ # 2. Heatmap -> [0,1] normalized, apply colormap (inferno: dark->bright)
72
+ hm = heatmap - heatmap.min()
73
+ hm = hm / (hm.max() + 1e-12) # [0,1]
74
+ heat_rgb = cm.inferno(hm)[..., :3] # (128,128,3), drop alpha
75
+
76
+ # 3. Blend: base frame + heatmap overlay
77
+ blended = (1 - alpha) * base_rgb + alpha * heat_rgb
78
+ blended = (np.clip(blended, 0, 1) * 255).astype(np.uint8)
79
+
80
+ # 4. To PNG -> base64
81
+ img = Image.fromarray(blended)
82
+ buf = io.BytesIO()
83
+ img.save(buf, format="PNG")
84
+ return base64.b64encode(buf.getvalue()).decode("utf-8")
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # App lifespan: load the model once at startup, not per request
89
+ # ---------------------------------------------------------------------------
90
+
91
+ # We store shared state on app.state so every request reuses it.
92
+ @asynccontextmanager
93
+ async def lifespan(app: FastAPI):
94
+ # Startup: verify the model file exists. (The ONNX session itself is created
95
+ # inside the stream pipeline per call; see the note in /predict below.)
96
+ if not os.path.exists(ONNX_PATH):
97
+ raise RuntimeError(f"ONNX model not found at {ONNX_PATH}. Run the export first.")
98
+ app.state.onnx_path = ONNX_PATH
99
+ app.state.threshold = THRESHOLD
100
+ yield
101
+ # Shutdown: nothing to clean up.
102
+
103
+
104
+ app = FastAPI(title="Video Anomaly Detection", lifespan=lifespan)
105
+
106
+ # Cors middleware
107
+ app.add_middleware(
108
+ CORSMiddleware,
109
+ allow_origins=["http://localhost:5173"],
110
+ allow_methods=["*"], allow_headers=["*"],
111
+ )
112
+
113
+ # Anomaly ratio per processed video (flagged frames / scored frames)
114
+ ANOMALY_RATIO = Gauge(
115
+ "augur_anomaly_ratio",
116
+ "Fraction of scored frames flagged as anomalous in the last request",
117
+ )
118
+
119
+ # Mean anomaly score per processed video — watch for drift vs the calibrated
120
+ # normal mean (~0.000153). A rising mean suggests the input distribution shifted.
121
+ MEAN_SCORE = Gauge(
122
+ "augur_mean_score",
123
+ "Mean per-frame anomaly score (raw MSE) of the last processed video",
124
+ )
125
+
126
+ # Distribution of per-frame scores — a histogram to see the spread, not just mean
127
+ SCORE_HIST = Histogram(
128
+ "augur_frame_score",
129
+ "Per-frame anomaly score (raw MSE) distribution",
130
+ buckets=[1e-4, 1.5e-4, 2e-4, 2.5e-4, 3e-4, 4e-4, 5e-4, 7e-4, 1e-3],
131
+ )
132
+
133
+ Instrumentator().instrument(app).expose(app)
134
+
135
+
136
+ # ---------------------------------------------------------------------------
137
+ # Routes
138
+ # ---------------------------------------------------------------------------
139
+
140
+ @app.get("/health")
141
+ def health():
142
+ """Simple liveness check."""
143
+ return {"status": "ok", "model": app.state.onnx_path, "threshold": app.state.threshold}
144
+
145
+
146
+ @app.post("/predict")
147
+ async def predict(file: UploadFile = File(...)):
148
+ """
149
+ Accept a video upload, run the streaming anomaly detector over it, and
150
+ return per-frame anomaly scores.
151
+
152
+ Response JSON:
153
+ {
154
+ "total_frames": int,
155
+ "scored_frames": int,
156
+ "warmup_frames": int, # first 15 frames have no score (cold start)
157
+ "threshold": float,
158
+ "frames": [
159
+ {"frame_idx": int, "score": float|null, "is_anomaly": bool|null}
160
+ ],
161
+ "top_anomalies": [{"frame_idx": int, "score": float, "overlay": "<base64 png>"}]
162
+ }
163
+ A null score / null is_anomaly marks a warm-up frame (model needs 15 past
164
+ frames before it can predict; those frames cannot be scored).
165
+ """
166
+ # Basic content-type guard (not bulletproof, just a friendly check).
167
+ if file.content_type is None or not file.content_type.startswith("video"):
168
+ raise HTTPException(status_code=400, detail="Please upload a video file.")
169
+
170
+ # cv2.VideoCapture needs a file path, so we write the upload to a temp file.
171
+ # Preserve the original suffix so the decoder picks the right backend.
172
+ suffix = os.path.splitext(file.filename or "")[1] or ".mp4"
173
+ tmp_path = None
174
+ try:
175
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
176
+ tmp.write(await file.read())
177
+ tmp_path = tmp.name
178
+
179
+ # Run the streaming pipeline (this is the code you wrote in stream.py).
180
+ # NOTE: process_video currently builds its own AnomalyStream (and thus
181
+ # its own ONNX session) per call. That is fine for a demo. To optimize
182
+ # later, refactor AnomalyStream to accept a pre-loaded session and share
183
+ # it across requests.
184
+ raw_scores, top_anomalies, fps = process_video(tmp_path, app.state.onnx_path)
185
+ fps = fps if fps and fps > 0 else 10.0 # fallback to 10 (UCSD default)
186
+
187
+ # Convert top anomalies to an overlay PNG
188
+ top = [
189
+ {
190
+ "frame_idx": a["frame_idx"],
191
+ "score": a["score"],
192
+ "overlay": make_overlay_png(a["frame"], a["heatmap"]),
193
+ }
194
+ for a in top_anomalies
195
+ ]
196
+
197
+ except Exception as exc:
198
+ raise HTTPException(status_code=500, detail=f"Inference failed: {exc}")
199
+ finally:
200
+ # Always clean up the temp file.
201
+ if tmp_path and os.path.exists(tmp_path):
202
+ os.remove(tmp_path)
203
+
204
+ # Build the per-frame response. raw_scores contains None for warm-up frames.
205
+ threshold = app.state.threshold
206
+ frames = []
207
+ scored = 0
208
+ for idx, score in enumerate(raw_scores):
209
+ if score is None:
210
+ frames.append({"frame_idx": idx, "score": None, "is_anomaly": None})
211
+ else:
212
+ scored += 1
213
+ frames.append({
214
+ "frame_idx": idx,
215
+ "score": float(score),
216
+ "is_anomaly": bool(score > threshold),
217
+ })
218
+
219
+ # Filter out the valid scores
220
+ valid_scores = [f["score"] for f in frames if f["score"] is not None]
221
+ if valid_scores:
222
+ flagged = sum(1 for f in frames if f["is_anomaly"])
223
+ ANOMALY_RATIO.set(flagged / len(valid_scores))
224
+ MEAN_SCORE.set(sum(valid_scores) / len(valid_scores))
225
+ for s in valid_scores:
226
+ SCORE_HIST.observe(s)
227
+
228
+ return {
229
+ "total_frames": len(raw_scores),
230
+ "scored_frames": scored,
231
+ "warmup_frames": len(raw_scores) - scored,
232
+ "threshold": threshold,
233
+ "fps": fps,
234
+ "frames": frames,
235
+ "top_anomalies": top
236
+ }
237
+
238
+ # Serve the built frontend. MUST come AFTER all API routes, otherwise the
239
+ # catch-all static mount would swallow /predict, /health, /metrics.
240
+ app.mount("/", StaticFiles(directory="static", html=True), name="static")
checkpoints/model.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:84a0715ff85849ebf88c8f6b320d96d7d0c66f5544881b79a984c573e761efd2
3
+ size 98186
checkpoints/model.onnx.data ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:74d4f799761990b7dbefc89095526fe0a9af5fb8d7c406d6842f27618442c562
3
+ size 3932160
frontend/index.html ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AUGUR — Predictive Anomaly Detection</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=JetBrains+Mono:wght@400;500;700&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ </head>
14
+ <body>
15
+ <div id="root"></div>
16
+ <script type="module" src="/src/main.jsx"></script>
17
+ </body>
18
+ </html>
frontend/package-lock.json ADDED
@@ -0,0 +1,2059 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "augur-frontend",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "augur-frontend",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "react": "^18.3.1",
12
+ "react-dom": "^18.3.1",
13
+ "recharts": "^2.12.7"
14
+ },
15
+ "devDependencies": {
16
+ "@vitejs/plugin-react": "^4.3.1",
17
+ "vite": "^5.4.0"
18
+ }
19
+ },
20
+ "node_modules/@babel/code-frame": {
21
+ "version": "7.29.7",
22
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
23
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
24
+ "dev": true,
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "@babel/helper-validator-identifier": "^7.29.7",
28
+ "js-tokens": "^4.0.0",
29
+ "picocolors": "^1.1.1"
30
+ },
31
+ "engines": {
32
+ "node": ">=6.9.0"
33
+ }
34
+ },
35
+ "node_modules/@babel/compat-data": {
36
+ "version": "7.29.7",
37
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
38
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
39
+ "dev": true,
40
+ "license": "MIT",
41
+ "engines": {
42
+ "node": ">=6.9.0"
43
+ }
44
+ },
45
+ "node_modules/@babel/core": {
46
+ "version": "7.29.7",
47
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
48
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
49
+ "dev": true,
50
+ "license": "MIT",
51
+ "dependencies": {
52
+ "@babel/code-frame": "^7.29.7",
53
+ "@babel/generator": "^7.29.7",
54
+ "@babel/helper-compilation-targets": "^7.29.7",
55
+ "@babel/helper-module-transforms": "^7.29.7",
56
+ "@babel/helpers": "^7.29.7",
57
+ "@babel/parser": "^7.29.7",
58
+ "@babel/template": "^7.29.7",
59
+ "@babel/traverse": "^7.29.7",
60
+ "@babel/types": "^7.29.7",
61
+ "@jridgewell/remapping": "^2.3.5",
62
+ "convert-source-map": "^2.0.0",
63
+ "debug": "^4.1.0",
64
+ "gensync": "^1.0.0-beta.2",
65
+ "json5": "^2.2.3",
66
+ "semver": "^6.3.1"
67
+ },
68
+ "engines": {
69
+ "node": ">=6.9.0"
70
+ },
71
+ "funding": {
72
+ "type": "opencollective",
73
+ "url": "https://opencollective.com/babel"
74
+ }
75
+ },
76
+ "node_modules/@babel/generator": {
77
+ "version": "7.29.7",
78
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
79
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
80
+ "dev": true,
81
+ "license": "MIT",
82
+ "dependencies": {
83
+ "@babel/parser": "^7.29.7",
84
+ "@babel/types": "^7.29.7",
85
+ "@jridgewell/gen-mapping": "^0.3.12",
86
+ "@jridgewell/trace-mapping": "^0.3.28",
87
+ "jsesc": "^3.0.2"
88
+ },
89
+ "engines": {
90
+ "node": ">=6.9.0"
91
+ }
92
+ },
93
+ "node_modules/@babel/helper-compilation-targets": {
94
+ "version": "7.29.7",
95
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
96
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
97
+ "dev": true,
98
+ "license": "MIT",
99
+ "dependencies": {
100
+ "@babel/compat-data": "^7.29.7",
101
+ "@babel/helper-validator-option": "^7.29.7",
102
+ "browserslist": "^4.24.0",
103
+ "lru-cache": "^5.1.1",
104
+ "semver": "^6.3.1"
105
+ },
106
+ "engines": {
107
+ "node": ">=6.9.0"
108
+ }
109
+ },
110
+ "node_modules/@babel/helper-globals": {
111
+ "version": "7.29.7",
112
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
113
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
114
+ "dev": true,
115
+ "license": "MIT",
116
+ "engines": {
117
+ "node": ">=6.9.0"
118
+ }
119
+ },
120
+ "node_modules/@babel/helper-module-imports": {
121
+ "version": "7.29.7",
122
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
123
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
124
+ "dev": true,
125
+ "license": "MIT",
126
+ "dependencies": {
127
+ "@babel/traverse": "^7.29.7",
128
+ "@babel/types": "^7.29.7"
129
+ },
130
+ "engines": {
131
+ "node": ">=6.9.0"
132
+ }
133
+ },
134
+ "node_modules/@babel/helper-module-transforms": {
135
+ "version": "7.29.7",
136
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
137
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
138
+ "dev": true,
139
+ "license": "MIT",
140
+ "dependencies": {
141
+ "@babel/helper-module-imports": "^7.29.7",
142
+ "@babel/helper-validator-identifier": "^7.29.7",
143
+ "@babel/traverse": "^7.29.7"
144
+ },
145
+ "engines": {
146
+ "node": ">=6.9.0"
147
+ },
148
+ "peerDependencies": {
149
+ "@babel/core": "^7.0.0"
150
+ }
151
+ },
152
+ "node_modules/@babel/helper-plugin-utils": {
153
+ "version": "7.29.7",
154
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
155
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
156
+ "dev": true,
157
+ "license": "MIT",
158
+ "engines": {
159
+ "node": ">=6.9.0"
160
+ }
161
+ },
162
+ "node_modules/@babel/helper-string-parser": {
163
+ "version": "7.29.7",
164
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
165
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
166
+ "dev": true,
167
+ "license": "MIT",
168
+ "engines": {
169
+ "node": ">=6.9.0"
170
+ }
171
+ },
172
+ "node_modules/@babel/helper-validator-identifier": {
173
+ "version": "7.29.7",
174
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
175
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
176
+ "dev": true,
177
+ "license": "MIT",
178
+ "engines": {
179
+ "node": ">=6.9.0"
180
+ }
181
+ },
182
+ "node_modules/@babel/helper-validator-option": {
183
+ "version": "7.29.7",
184
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
185
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
186
+ "dev": true,
187
+ "license": "MIT",
188
+ "engines": {
189
+ "node": ">=6.9.0"
190
+ }
191
+ },
192
+ "node_modules/@babel/helpers": {
193
+ "version": "7.29.7",
194
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
195
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
196
+ "dev": true,
197
+ "license": "MIT",
198
+ "dependencies": {
199
+ "@babel/template": "^7.29.7",
200
+ "@babel/types": "^7.29.7"
201
+ },
202
+ "engines": {
203
+ "node": ">=6.9.0"
204
+ }
205
+ },
206
+ "node_modules/@babel/parser": {
207
+ "version": "7.29.7",
208
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
209
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
210
+ "dev": true,
211
+ "license": "MIT",
212
+ "dependencies": {
213
+ "@babel/types": "^7.29.7"
214
+ },
215
+ "bin": {
216
+ "parser": "bin/babel-parser.js"
217
+ },
218
+ "engines": {
219
+ "node": ">=6.0.0"
220
+ }
221
+ },
222
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
223
+ "version": "7.29.7",
224
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
225
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
226
+ "dev": true,
227
+ "license": "MIT",
228
+ "dependencies": {
229
+ "@babel/helper-plugin-utils": "^7.29.7"
230
+ },
231
+ "engines": {
232
+ "node": ">=6.9.0"
233
+ },
234
+ "peerDependencies": {
235
+ "@babel/core": "^7.0.0-0"
236
+ }
237
+ },
238
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
239
+ "version": "7.29.7",
240
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
241
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
242
+ "dev": true,
243
+ "license": "MIT",
244
+ "dependencies": {
245
+ "@babel/helper-plugin-utils": "^7.29.7"
246
+ },
247
+ "engines": {
248
+ "node": ">=6.9.0"
249
+ },
250
+ "peerDependencies": {
251
+ "@babel/core": "^7.0.0-0"
252
+ }
253
+ },
254
+ "node_modules/@babel/runtime": {
255
+ "version": "7.29.7",
256
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
257
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
258
+ "license": "MIT",
259
+ "engines": {
260
+ "node": ">=6.9.0"
261
+ }
262
+ },
263
+ "node_modules/@babel/template": {
264
+ "version": "7.29.7",
265
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
266
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
267
+ "dev": true,
268
+ "license": "MIT",
269
+ "dependencies": {
270
+ "@babel/code-frame": "^7.29.7",
271
+ "@babel/parser": "^7.29.7",
272
+ "@babel/types": "^7.29.7"
273
+ },
274
+ "engines": {
275
+ "node": ">=6.9.0"
276
+ }
277
+ },
278
+ "node_modules/@babel/traverse": {
279
+ "version": "7.29.7",
280
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
281
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
282
+ "dev": true,
283
+ "license": "MIT",
284
+ "dependencies": {
285
+ "@babel/code-frame": "^7.29.7",
286
+ "@babel/generator": "^7.29.7",
287
+ "@babel/helper-globals": "^7.29.7",
288
+ "@babel/parser": "^7.29.7",
289
+ "@babel/template": "^7.29.7",
290
+ "@babel/types": "^7.29.7",
291
+ "debug": "^4.3.1"
292
+ },
293
+ "engines": {
294
+ "node": ">=6.9.0"
295
+ }
296
+ },
297
+ "node_modules/@babel/types": {
298
+ "version": "7.29.7",
299
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
300
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
301
+ "dev": true,
302
+ "license": "MIT",
303
+ "dependencies": {
304
+ "@babel/helper-string-parser": "^7.29.7",
305
+ "@babel/helper-validator-identifier": "^7.29.7"
306
+ },
307
+ "engines": {
308
+ "node": ">=6.9.0"
309
+ }
310
+ },
311
+ "node_modules/@esbuild/aix-ppc64": {
312
+ "version": "0.21.5",
313
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
314
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
315
+ "cpu": [
316
+ "ppc64"
317
+ ],
318
+ "dev": true,
319
+ "license": "MIT",
320
+ "optional": true,
321
+ "os": [
322
+ "aix"
323
+ ],
324
+ "engines": {
325
+ "node": ">=12"
326
+ }
327
+ },
328
+ "node_modules/@esbuild/android-arm": {
329
+ "version": "0.21.5",
330
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
331
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
332
+ "cpu": [
333
+ "arm"
334
+ ],
335
+ "dev": true,
336
+ "license": "MIT",
337
+ "optional": true,
338
+ "os": [
339
+ "android"
340
+ ],
341
+ "engines": {
342
+ "node": ">=12"
343
+ }
344
+ },
345
+ "node_modules/@esbuild/android-arm64": {
346
+ "version": "0.21.5",
347
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
348
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
349
+ "cpu": [
350
+ "arm64"
351
+ ],
352
+ "dev": true,
353
+ "license": "MIT",
354
+ "optional": true,
355
+ "os": [
356
+ "android"
357
+ ],
358
+ "engines": {
359
+ "node": ">=12"
360
+ }
361
+ },
362
+ "node_modules/@esbuild/android-x64": {
363
+ "version": "0.21.5",
364
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
365
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
366
+ "cpu": [
367
+ "x64"
368
+ ],
369
+ "dev": true,
370
+ "license": "MIT",
371
+ "optional": true,
372
+ "os": [
373
+ "android"
374
+ ],
375
+ "engines": {
376
+ "node": ">=12"
377
+ }
378
+ },
379
+ "node_modules/@esbuild/darwin-arm64": {
380
+ "version": "0.21.5",
381
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
382
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
383
+ "cpu": [
384
+ "arm64"
385
+ ],
386
+ "dev": true,
387
+ "license": "MIT",
388
+ "optional": true,
389
+ "os": [
390
+ "darwin"
391
+ ],
392
+ "engines": {
393
+ "node": ">=12"
394
+ }
395
+ },
396
+ "node_modules/@esbuild/darwin-x64": {
397
+ "version": "0.21.5",
398
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
399
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
400
+ "cpu": [
401
+ "x64"
402
+ ],
403
+ "dev": true,
404
+ "license": "MIT",
405
+ "optional": true,
406
+ "os": [
407
+ "darwin"
408
+ ],
409
+ "engines": {
410
+ "node": ">=12"
411
+ }
412
+ },
413
+ "node_modules/@esbuild/freebsd-arm64": {
414
+ "version": "0.21.5",
415
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
416
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
417
+ "cpu": [
418
+ "arm64"
419
+ ],
420
+ "dev": true,
421
+ "license": "MIT",
422
+ "optional": true,
423
+ "os": [
424
+ "freebsd"
425
+ ],
426
+ "engines": {
427
+ "node": ">=12"
428
+ }
429
+ },
430
+ "node_modules/@esbuild/freebsd-x64": {
431
+ "version": "0.21.5",
432
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
433
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
434
+ "cpu": [
435
+ "x64"
436
+ ],
437
+ "dev": true,
438
+ "license": "MIT",
439
+ "optional": true,
440
+ "os": [
441
+ "freebsd"
442
+ ],
443
+ "engines": {
444
+ "node": ">=12"
445
+ }
446
+ },
447
+ "node_modules/@esbuild/linux-arm": {
448
+ "version": "0.21.5",
449
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
450
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
451
+ "cpu": [
452
+ "arm"
453
+ ],
454
+ "dev": true,
455
+ "license": "MIT",
456
+ "optional": true,
457
+ "os": [
458
+ "linux"
459
+ ],
460
+ "engines": {
461
+ "node": ">=12"
462
+ }
463
+ },
464
+ "node_modules/@esbuild/linux-arm64": {
465
+ "version": "0.21.5",
466
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
467
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
468
+ "cpu": [
469
+ "arm64"
470
+ ],
471
+ "dev": true,
472
+ "license": "MIT",
473
+ "optional": true,
474
+ "os": [
475
+ "linux"
476
+ ],
477
+ "engines": {
478
+ "node": ">=12"
479
+ }
480
+ },
481
+ "node_modules/@esbuild/linux-ia32": {
482
+ "version": "0.21.5",
483
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
484
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
485
+ "cpu": [
486
+ "ia32"
487
+ ],
488
+ "dev": true,
489
+ "license": "MIT",
490
+ "optional": true,
491
+ "os": [
492
+ "linux"
493
+ ],
494
+ "engines": {
495
+ "node": ">=12"
496
+ }
497
+ },
498
+ "node_modules/@esbuild/linux-loong64": {
499
+ "version": "0.21.5",
500
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
501
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
502
+ "cpu": [
503
+ "loong64"
504
+ ],
505
+ "dev": true,
506
+ "license": "MIT",
507
+ "optional": true,
508
+ "os": [
509
+ "linux"
510
+ ],
511
+ "engines": {
512
+ "node": ">=12"
513
+ }
514
+ },
515
+ "node_modules/@esbuild/linux-mips64el": {
516
+ "version": "0.21.5",
517
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
518
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
519
+ "cpu": [
520
+ "mips64el"
521
+ ],
522
+ "dev": true,
523
+ "license": "MIT",
524
+ "optional": true,
525
+ "os": [
526
+ "linux"
527
+ ],
528
+ "engines": {
529
+ "node": ">=12"
530
+ }
531
+ },
532
+ "node_modules/@esbuild/linux-ppc64": {
533
+ "version": "0.21.5",
534
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
535
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
536
+ "cpu": [
537
+ "ppc64"
538
+ ],
539
+ "dev": true,
540
+ "license": "MIT",
541
+ "optional": true,
542
+ "os": [
543
+ "linux"
544
+ ],
545
+ "engines": {
546
+ "node": ">=12"
547
+ }
548
+ },
549
+ "node_modules/@esbuild/linux-riscv64": {
550
+ "version": "0.21.5",
551
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
552
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
553
+ "cpu": [
554
+ "riscv64"
555
+ ],
556
+ "dev": true,
557
+ "license": "MIT",
558
+ "optional": true,
559
+ "os": [
560
+ "linux"
561
+ ],
562
+ "engines": {
563
+ "node": ">=12"
564
+ }
565
+ },
566
+ "node_modules/@esbuild/linux-s390x": {
567
+ "version": "0.21.5",
568
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
569
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
570
+ "cpu": [
571
+ "s390x"
572
+ ],
573
+ "dev": true,
574
+ "license": "MIT",
575
+ "optional": true,
576
+ "os": [
577
+ "linux"
578
+ ],
579
+ "engines": {
580
+ "node": ">=12"
581
+ }
582
+ },
583
+ "node_modules/@esbuild/linux-x64": {
584
+ "version": "0.21.5",
585
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
586
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
587
+ "cpu": [
588
+ "x64"
589
+ ],
590
+ "dev": true,
591
+ "license": "MIT",
592
+ "optional": true,
593
+ "os": [
594
+ "linux"
595
+ ],
596
+ "engines": {
597
+ "node": ">=12"
598
+ }
599
+ },
600
+ "node_modules/@esbuild/netbsd-x64": {
601
+ "version": "0.21.5",
602
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
603
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
604
+ "cpu": [
605
+ "x64"
606
+ ],
607
+ "dev": true,
608
+ "license": "MIT",
609
+ "optional": true,
610
+ "os": [
611
+ "netbsd"
612
+ ],
613
+ "engines": {
614
+ "node": ">=12"
615
+ }
616
+ },
617
+ "node_modules/@esbuild/openbsd-x64": {
618
+ "version": "0.21.5",
619
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
620
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
621
+ "cpu": [
622
+ "x64"
623
+ ],
624
+ "dev": true,
625
+ "license": "MIT",
626
+ "optional": true,
627
+ "os": [
628
+ "openbsd"
629
+ ],
630
+ "engines": {
631
+ "node": ">=12"
632
+ }
633
+ },
634
+ "node_modules/@esbuild/sunos-x64": {
635
+ "version": "0.21.5",
636
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
637
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
638
+ "cpu": [
639
+ "x64"
640
+ ],
641
+ "dev": true,
642
+ "license": "MIT",
643
+ "optional": true,
644
+ "os": [
645
+ "sunos"
646
+ ],
647
+ "engines": {
648
+ "node": ">=12"
649
+ }
650
+ },
651
+ "node_modules/@esbuild/win32-arm64": {
652
+ "version": "0.21.5",
653
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
654
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
655
+ "cpu": [
656
+ "arm64"
657
+ ],
658
+ "dev": true,
659
+ "license": "MIT",
660
+ "optional": true,
661
+ "os": [
662
+ "win32"
663
+ ],
664
+ "engines": {
665
+ "node": ">=12"
666
+ }
667
+ },
668
+ "node_modules/@esbuild/win32-ia32": {
669
+ "version": "0.21.5",
670
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
671
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
672
+ "cpu": [
673
+ "ia32"
674
+ ],
675
+ "dev": true,
676
+ "license": "MIT",
677
+ "optional": true,
678
+ "os": [
679
+ "win32"
680
+ ],
681
+ "engines": {
682
+ "node": ">=12"
683
+ }
684
+ },
685
+ "node_modules/@esbuild/win32-x64": {
686
+ "version": "0.21.5",
687
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
688
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
689
+ "cpu": [
690
+ "x64"
691
+ ],
692
+ "dev": true,
693
+ "license": "MIT",
694
+ "optional": true,
695
+ "os": [
696
+ "win32"
697
+ ],
698
+ "engines": {
699
+ "node": ">=12"
700
+ }
701
+ },
702
+ "node_modules/@jridgewell/gen-mapping": {
703
+ "version": "0.3.13",
704
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
705
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
706
+ "dev": true,
707
+ "license": "MIT",
708
+ "dependencies": {
709
+ "@jridgewell/sourcemap-codec": "^1.5.0",
710
+ "@jridgewell/trace-mapping": "^0.3.24"
711
+ }
712
+ },
713
+ "node_modules/@jridgewell/remapping": {
714
+ "version": "2.3.5",
715
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
716
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
717
+ "dev": true,
718
+ "license": "MIT",
719
+ "dependencies": {
720
+ "@jridgewell/gen-mapping": "^0.3.5",
721
+ "@jridgewell/trace-mapping": "^0.3.24"
722
+ }
723
+ },
724
+ "node_modules/@jridgewell/resolve-uri": {
725
+ "version": "3.1.2",
726
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
727
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
728
+ "dev": true,
729
+ "license": "MIT",
730
+ "engines": {
731
+ "node": ">=6.0.0"
732
+ }
733
+ },
734
+ "node_modules/@jridgewell/sourcemap-codec": {
735
+ "version": "1.5.5",
736
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
737
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
738
+ "dev": true,
739
+ "license": "MIT"
740
+ },
741
+ "node_modules/@jridgewell/trace-mapping": {
742
+ "version": "0.3.31",
743
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
744
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
745
+ "dev": true,
746
+ "license": "MIT",
747
+ "dependencies": {
748
+ "@jridgewell/resolve-uri": "^3.1.0",
749
+ "@jridgewell/sourcemap-codec": "^1.4.14"
750
+ }
751
+ },
752
+ "node_modules/@rolldown/pluginutils": {
753
+ "version": "1.0.0-beta.27",
754
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
755
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
756
+ "dev": true,
757
+ "license": "MIT"
758
+ },
759
+ "node_modules/@rollup/rollup-android-arm-eabi": {
760
+ "version": "4.62.0",
761
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz",
762
+ "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==",
763
+ "cpu": [
764
+ "arm"
765
+ ],
766
+ "dev": true,
767
+ "license": "MIT",
768
+ "optional": true,
769
+ "os": [
770
+ "android"
771
+ ]
772
+ },
773
+ "node_modules/@rollup/rollup-android-arm64": {
774
+ "version": "4.62.0",
775
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz",
776
+ "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==",
777
+ "cpu": [
778
+ "arm64"
779
+ ],
780
+ "dev": true,
781
+ "license": "MIT",
782
+ "optional": true,
783
+ "os": [
784
+ "android"
785
+ ]
786
+ },
787
+ "node_modules/@rollup/rollup-darwin-arm64": {
788
+ "version": "4.62.0",
789
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz",
790
+ "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==",
791
+ "cpu": [
792
+ "arm64"
793
+ ],
794
+ "dev": true,
795
+ "license": "MIT",
796
+ "optional": true,
797
+ "os": [
798
+ "darwin"
799
+ ]
800
+ },
801
+ "node_modules/@rollup/rollup-darwin-x64": {
802
+ "version": "4.62.0",
803
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz",
804
+ "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==",
805
+ "cpu": [
806
+ "x64"
807
+ ],
808
+ "dev": true,
809
+ "license": "MIT",
810
+ "optional": true,
811
+ "os": [
812
+ "darwin"
813
+ ]
814
+ },
815
+ "node_modules/@rollup/rollup-freebsd-arm64": {
816
+ "version": "4.62.0",
817
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz",
818
+ "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==",
819
+ "cpu": [
820
+ "arm64"
821
+ ],
822
+ "dev": true,
823
+ "license": "MIT",
824
+ "optional": true,
825
+ "os": [
826
+ "freebsd"
827
+ ]
828
+ },
829
+ "node_modules/@rollup/rollup-freebsd-x64": {
830
+ "version": "4.62.0",
831
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz",
832
+ "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==",
833
+ "cpu": [
834
+ "x64"
835
+ ],
836
+ "dev": true,
837
+ "license": "MIT",
838
+ "optional": true,
839
+ "os": [
840
+ "freebsd"
841
+ ]
842
+ },
843
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
844
+ "version": "4.62.0",
845
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz",
846
+ "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==",
847
+ "cpu": [
848
+ "arm"
849
+ ],
850
+ "dev": true,
851
+ "license": "MIT",
852
+ "optional": true,
853
+ "os": [
854
+ "linux"
855
+ ]
856
+ },
857
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
858
+ "version": "4.62.0",
859
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz",
860
+ "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==",
861
+ "cpu": [
862
+ "arm"
863
+ ],
864
+ "dev": true,
865
+ "license": "MIT",
866
+ "optional": true,
867
+ "os": [
868
+ "linux"
869
+ ]
870
+ },
871
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
872
+ "version": "4.62.0",
873
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz",
874
+ "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==",
875
+ "cpu": [
876
+ "arm64"
877
+ ],
878
+ "dev": true,
879
+ "license": "MIT",
880
+ "optional": true,
881
+ "os": [
882
+ "linux"
883
+ ]
884
+ },
885
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
886
+ "version": "4.62.0",
887
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz",
888
+ "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==",
889
+ "cpu": [
890
+ "arm64"
891
+ ],
892
+ "dev": true,
893
+ "license": "MIT",
894
+ "optional": true,
895
+ "os": [
896
+ "linux"
897
+ ]
898
+ },
899
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
900
+ "version": "4.62.0",
901
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz",
902
+ "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==",
903
+ "cpu": [
904
+ "loong64"
905
+ ],
906
+ "dev": true,
907
+ "license": "MIT",
908
+ "optional": true,
909
+ "os": [
910
+ "linux"
911
+ ]
912
+ },
913
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
914
+ "version": "4.62.0",
915
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz",
916
+ "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==",
917
+ "cpu": [
918
+ "loong64"
919
+ ],
920
+ "dev": true,
921
+ "license": "MIT",
922
+ "optional": true,
923
+ "os": [
924
+ "linux"
925
+ ]
926
+ },
927
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
928
+ "version": "4.62.0",
929
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz",
930
+ "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==",
931
+ "cpu": [
932
+ "ppc64"
933
+ ],
934
+ "dev": true,
935
+ "license": "MIT",
936
+ "optional": true,
937
+ "os": [
938
+ "linux"
939
+ ]
940
+ },
941
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
942
+ "version": "4.62.0",
943
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz",
944
+ "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==",
945
+ "cpu": [
946
+ "ppc64"
947
+ ],
948
+ "dev": true,
949
+ "license": "MIT",
950
+ "optional": true,
951
+ "os": [
952
+ "linux"
953
+ ]
954
+ },
955
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
956
+ "version": "4.62.0",
957
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz",
958
+ "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==",
959
+ "cpu": [
960
+ "riscv64"
961
+ ],
962
+ "dev": true,
963
+ "license": "MIT",
964
+ "optional": true,
965
+ "os": [
966
+ "linux"
967
+ ]
968
+ },
969
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
970
+ "version": "4.62.0",
971
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz",
972
+ "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==",
973
+ "cpu": [
974
+ "riscv64"
975
+ ],
976
+ "dev": true,
977
+ "license": "MIT",
978
+ "optional": true,
979
+ "os": [
980
+ "linux"
981
+ ]
982
+ },
983
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
984
+ "version": "4.62.0",
985
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz",
986
+ "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==",
987
+ "cpu": [
988
+ "s390x"
989
+ ],
990
+ "dev": true,
991
+ "license": "MIT",
992
+ "optional": true,
993
+ "os": [
994
+ "linux"
995
+ ]
996
+ },
997
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
998
+ "version": "4.62.0",
999
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz",
1000
+ "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==",
1001
+ "cpu": [
1002
+ "x64"
1003
+ ],
1004
+ "dev": true,
1005
+ "license": "MIT",
1006
+ "optional": true,
1007
+ "os": [
1008
+ "linux"
1009
+ ]
1010
+ },
1011
+ "node_modules/@rollup/rollup-linux-x64-musl": {
1012
+ "version": "4.62.0",
1013
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz",
1014
+ "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==",
1015
+ "cpu": [
1016
+ "x64"
1017
+ ],
1018
+ "dev": true,
1019
+ "license": "MIT",
1020
+ "optional": true,
1021
+ "os": [
1022
+ "linux"
1023
+ ]
1024
+ },
1025
+ "node_modules/@rollup/rollup-openbsd-x64": {
1026
+ "version": "4.62.0",
1027
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz",
1028
+ "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==",
1029
+ "cpu": [
1030
+ "x64"
1031
+ ],
1032
+ "dev": true,
1033
+ "license": "MIT",
1034
+ "optional": true,
1035
+ "os": [
1036
+ "openbsd"
1037
+ ]
1038
+ },
1039
+ "node_modules/@rollup/rollup-openharmony-arm64": {
1040
+ "version": "4.62.0",
1041
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz",
1042
+ "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==",
1043
+ "cpu": [
1044
+ "arm64"
1045
+ ],
1046
+ "dev": true,
1047
+ "license": "MIT",
1048
+ "optional": true,
1049
+ "os": [
1050
+ "openharmony"
1051
+ ]
1052
+ },
1053
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
1054
+ "version": "4.62.0",
1055
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz",
1056
+ "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==",
1057
+ "cpu": [
1058
+ "arm64"
1059
+ ],
1060
+ "dev": true,
1061
+ "license": "MIT",
1062
+ "optional": true,
1063
+ "os": [
1064
+ "win32"
1065
+ ]
1066
+ },
1067
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
1068
+ "version": "4.62.0",
1069
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz",
1070
+ "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==",
1071
+ "cpu": [
1072
+ "ia32"
1073
+ ],
1074
+ "dev": true,
1075
+ "license": "MIT",
1076
+ "optional": true,
1077
+ "os": [
1078
+ "win32"
1079
+ ]
1080
+ },
1081
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
1082
+ "version": "4.62.0",
1083
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz",
1084
+ "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==",
1085
+ "cpu": [
1086
+ "x64"
1087
+ ],
1088
+ "dev": true,
1089
+ "license": "MIT",
1090
+ "optional": true,
1091
+ "os": [
1092
+ "win32"
1093
+ ]
1094
+ },
1095
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
1096
+ "version": "4.62.0",
1097
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz",
1098
+ "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==",
1099
+ "cpu": [
1100
+ "x64"
1101
+ ],
1102
+ "dev": true,
1103
+ "license": "MIT",
1104
+ "optional": true,
1105
+ "os": [
1106
+ "win32"
1107
+ ]
1108
+ },
1109
+ "node_modules/@types/babel__core": {
1110
+ "version": "7.20.5",
1111
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1112
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1113
+ "dev": true,
1114
+ "license": "MIT",
1115
+ "dependencies": {
1116
+ "@babel/parser": "^7.20.7",
1117
+ "@babel/types": "^7.20.7",
1118
+ "@types/babel__generator": "*",
1119
+ "@types/babel__template": "*",
1120
+ "@types/babel__traverse": "*"
1121
+ }
1122
+ },
1123
+ "node_modules/@types/babel__generator": {
1124
+ "version": "7.27.0",
1125
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1126
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1127
+ "dev": true,
1128
+ "license": "MIT",
1129
+ "dependencies": {
1130
+ "@babel/types": "^7.0.0"
1131
+ }
1132
+ },
1133
+ "node_modules/@types/babel__template": {
1134
+ "version": "7.4.4",
1135
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1136
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1137
+ "dev": true,
1138
+ "license": "MIT",
1139
+ "dependencies": {
1140
+ "@babel/parser": "^7.1.0",
1141
+ "@babel/types": "^7.0.0"
1142
+ }
1143
+ },
1144
+ "node_modules/@types/babel__traverse": {
1145
+ "version": "7.28.0",
1146
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
1147
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
1148
+ "dev": true,
1149
+ "license": "MIT",
1150
+ "dependencies": {
1151
+ "@babel/types": "^7.28.2"
1152
+ }
1153
+ },
1154
+ "node_modules/@types/d3-array": {
1155
+ "version": "3.2.2",
1156
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
1157
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
1158
+ "license": "MIT"
1159
+ },
1160
+ "node_modules/@types/d3-color": {
1161
+ "version": "3.1.3",
1162
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
1163
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
1164
+ "license": "MIT"
1165
+ },
1166
+ "node_modules/@types/d3-ease": {
1167
+ "version": "3.0.2",
1168
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
1169
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
1170
+ "license": "MIT"
1171
+ },
1172
+ "node_modules/@types/d3-interpolate": {
1173
+ "version": "3.0.4",
1174
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
1175
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
1176
+ "license": "MIT",
1177
+ "dependencies": {
1178
+ "@types/d3-color": "*"
1179
+ }
1180
+ },
1181
+ "node_modules/@types/d3-path": {
1182
+ "version": "3.1.1",
1183
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
1184
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
1185
+ "license": "MIT"
1186
+ },
1187
+ "node_modules/@types/d3-scale": {
1188
+ "version": "4.0.9",
1189
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
1190
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
1191
+ "license": "MIT",
1192
+ "dependencies": {
1193
+ "@types/d3-time": "*"
1194
+ }
1195
+ },
1196
+ "node_modules/@types/d3-shape": {
1197
+ "version": "3.1.8",
1198
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
1199
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
1200
+ "license": "MIT",
1201
+ "dependencies": {
1202
+ "@types/d3-path": "*"
1203
+ }
1204
+ },
1205
+ "node_modules/@types/d3-time": {
1206
+ "version": "3.0.4",
1207
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
1208
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
1209
+ "license": "MIT"
1210
+ },
1211
+ "node_modules/@types/d3-timer": {
1212
+ "version": "3.0.2",
1213
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
1214
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
1215
+ "license": "MIT"
1216
+ },
1217
+ "node_modules/@types/estree": {
1218
+ "version": "1.0.9",
1219
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
1220
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
1221
+ "dev": true,
1222
+ "license": "MIT"
1223
+ },
1224
+ "node_modules/@vitejs/plugin-react": {
1225
+ "version": "4.7.0",
1226
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
1227
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
1228
+ "dev": true,
1229
+ "license": "MIT",
1230
+ "dependencies": {
1231
+ "@babel/core": "^7.28.0",
1232
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
1233
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
1234
+ "@rolldown/pluginutils": "1.0.0-beta.27",
1235
+ "@types/babel__core": "^7.20.5",
1236
+ "react-refresh": "^0.17.0"
1237
+ },
1238
+ "engines": {
1239
+ "node": "^14.18.0 || >=16.0.0"
1240
+ },
1241
+ "peerDependencies": {
1242
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
1243
+ }
1244
+ },
1245
+ "node_modules/baseline-browser-mapping": {
1246
+ "version": "2.10.37",
1247
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz",
1248
+ "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==",
1249
+ "dev": true,
1250
+ "license": "Apache-2.0",
1251
+ "bin": {
1252
+ "baseline-browser-mapping": "dist/cli.cjs"
1253
+ },
1254
+ "engines": {
1255
+ "node": ">=6.0.0"
1256
+ }
1257
+ },
1258
+ "node_modules/browserslist": {
1259
+ "version": "4.28.2",
1260
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
1261
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
1262
+ "dev": true,
1263
+ "funding": [
1264
+ {
1265
+ "type": "opencollective",
1266
+ "url": "https://opencollective.com/browserslist"
1267
+ },
1268
+ {
1269
+ "type": "tidelift",
1270
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1271
+ },
1272
+ {
1273
+ "type": "github",
1274
+ "url": "https://github.com/sponsors/ai"
1275
+ }
1276
+ ],
1277
+ "license": "MIT",
1278
+ "dependencies": {
1279
+ "baseline-browser-mapping": "^2.10.12",
1280
+ "caniuse-lite": "^1.0.30001782",
1281
+ "electron-to-chromium": "^1.5.328",
1282
+ "node-releases": "^2.0.36",
1283
+ "update-browserslist-db": "^1.2.3"
1284
+ },
1285
+ "bin": {
1286
+ "browserslist": "cli.js"
1287
+ },
1288
+ "engines": {
1289
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1290
+ }
1291
+ },
1292
+ "node_modules/caniuse-lite": {
1293
+ "version": "1.0.30001799",
1294
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz",
1295
+ "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==",
1296
+ "dev": true,
1297
+ "funding": [
1298
+ {
1299
+ "type": "opencollective",
1300
+ "url": "https://opencollective.com/browserslist"
1301
+ },
1302
+ {
1303
+ "type": "tidelift",
1304
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1305
+ },
1306
+ {
1307
+ "type": "github",
1308
+ "url": "https://github.com/sponsors/ai"
1309
+ }
1310
+ ],
1311
+ "license": "CC-BY-4.0"
1312
+ },
1313
+ "node_modules/clsx": {
1314
+ "version": "2.1.1",
1315
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
1316
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
1317
+ "license": "MIT",
1318
+ "engines": {
1319
+ "node": ">=6"
1320
+ }
1321
+ },
1322
+ "node_modules/convert-source-map": {
1323
+ "version": "2.0.0",
1324
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1325
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1326
+ "dev": true,
1327
+ "license": "MIT"
1328
+ },
1329
+ "node_modules/csstype": {
1330
+ "version": "3.2.3",
1331
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
1332
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
1333
+ "license": "MIT"
1334
+ },
1335
+ "node_modules/d3-array": {
1336
+ "version": "3.2.4",
1337
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
1338
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
1339
+ "license": "ISC",
1340
+ "dependencies": {
1341
+ "internmap": "1 - 2"
1342
+ },
1343
+ "engines": {
1344
+ "node": ">=12"
1345
+ }
1346
+ },
1347
+ "node_modules/d3-color": {
1348
+ "version": "3.1.0",
1349
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
1350
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
1351
+ "license": "ISC",
1352
+ "engines": {
1353
+ "node": ">=12"
1354
+ }
1355
+ },
1356
+ "node_modules/d3-ease": {
1357
+ "version": "3.0.1",
1358
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
1359
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
1360
+ "license": "BSD-3-Clause",
1361
+ "engines": {
1362
+ "node": ">=12"
1363
+ }
1364
+ },
1365
+ "node_modules/d3-format": {
1366
+ "version": "3.1.2",
1367
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
1368
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
1369
+ "license": "ISC",
1370
+ "engines": {
1371
+ "node": ">=12"
1372
+ }
1373
+ },
1374
+ "node_modules/d3-interpolate": {
1375
+ "version": "3.0.1",
1376
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
1377
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
1378
+ "license": "ISC",
1379
+ "dependencies": {
1380
+ "d3-color": "1 - 3"
1381
+ },
1382
+ "engines": {
1383
+ "node": ">=12"
1384
+ }
1385
+ },
1386
+ "node_modules/d3-path": {
1387
+ "version": "3.1.0",
1388
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
1389
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
1390
+ "license": "ISC",
1391
+ "engines": {
1392
+ "node": ">=12"
1393
+ }
1394
+ },
1395
+ "node_modules/d3-scale": {
1396
+ "version": "4.0.2",
1397
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
1398
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
1399
+ "license": "ISC",
1400
+ "dependencies": {
1401
+ "d3-array": "2.10.0 - 3",
1402
+ "d3-format": "1 - 3",
1403
+ "d3-interpolate": "1.2.0 - 3",
1404
+ "d3-time": "2.1.1 - 3",
1405
+ "d3-time-format": "2 - 4"
1406
+ },
1407
+ "engines": {
1408
+ "node": ">=12"
1409
+ }
1410
+ },
1411
+ "node_modules/d3-shape": {
1412
+ "version": "3.2.0",
1413
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
1414
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
1415
+ "license": "ISC",
1416
+ "dependencies": {
1417
+ "d3-path": "^3.1.0"
1418
+ },
1419
+ "engines": {
1420
+ "node": ">=12"
1421
+ }
1422
+ },
1423
+ "node_modules/d3-time": {
1424
+ "version": "3.1.0",
1425
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
1426
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
1427
+ "license": "ISC",
1428
+ "dependencies": {
1429
+ "d3-array": "2 - 3"
1430
+ },
1431
+ "engines": {
1432
+ "node": ">=12"
1433
+ }
1434
+ },
1435
+ "node_modules/d3-time-format": {
1436
+ "version": "4.1.0",
1437
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
1438
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
1439
+ "license": "ISC",
1440
+ "dependencies": {
1441
+ "d3-time": "1 - 3"
1442
+ },
1443
+ "engines": {
1444
+ "node": ">=12"
1445
+ }
1446
+ },
1447
+ "node_modules/d3-timer": {
1448
+ "version": "3.0.1",
1449
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
1450
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
1451
+ "license": "ISC",
1452
+ "engines": {
1453
+ "node": ">=12"
1454
+ }
1455
+ },
1456
+ "node_modules/debug": {
1457
+ "version": "4.4.3",
1458
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
1459
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
1460
+ "dev": true,
1461
+ "license": "MIT",
1462
+ "dependencies": {
1463
+ "ms": "^2.1.3"
1464
+ },
1465
+ "engines": {
1466
+ "node": ">=6.0"
1467
+ },
1468
+ "peerDependenciesMeta": {
1469
+ "supports-color": {
1470
+ "optional": true
1471
+ }
1472
+ }
1473
+ },
1474
+ "node_modules/decimal.js-light": {
1475
+ "version": "2.5.1",
1476
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
1477
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
1478
+ "license": "MIT"
1479
+ },
1480
+ "node_modules/dom-helpers": {
1481
+ "version": "5.2.1",
1482
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
1483
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
1484
+ "license": "MIT",
1485
+ "dependencies": {
1486
+ "@babel/runtime": "^7.8.7",
1487
+ "csstype": "^3.0.2"
1488
+ }
1489
+ },
1490
+ "node_modules/electron-to-chromium": {
1491
+ "version": "1.5.372",
1492
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz",
1493
+ "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==",
1494
+ "dev": true,
1495
+ "license": "ISC"
1496
+ },
1497
+ "node_modules/esbuild": {
1498
+ "version": "0.21.5",
1499
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
1500
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
1501
+ "dev": true,
1502
+ "hasInstallScript": true,
1503
+ "license": "MIT",
1504
+ "bin": {
1505
+ "esbuild": "bin/esbuild"
1506
+ },
1507
+ "engines": {
1508
+ "node": ">=12"
1509
+ },
1510
+ "optionalDependencies": {
1511
+ "@esbuild/aix-ppc64": "0.21.5",
1512
+ "@esbuild/android-arm": "0.21.5",
1513
+ "@esbuild/android-arm64": "0.21.5",
1514
+ "@esbuild/android-x64": "0.21.5",
1515
+ "@esbuild/darwin-arm64": "0.21.5",
1516
+ "@esbuild/darwin-x64": "0.21.5",
1517
+ "@esbuild/freebsd-arm64": "0.21.5",
1518
+ "@esbuild/freebsd-x64": "0.21.5",
1519
+ "@esbuild/linux-arm": "0.21.5",
1520
+ "@esbuild/linux-arm64": "0.21.5",
1521
+ "@esbuild/linux-ia32": "0.21.5",
1522
+ "@esbuild/linux-loong64": "0.21.5",
1523
+ "@esbuild/linux-mips64el": "0.21.5",
1524
+ "@esbuild/linux-ppc64": "0.21.5",
1525
+ "@esbuild/linux-riscv64": "0.21.5",
1526
+ "@esbuild/linux-s390x": "0.21.5",
1527
+ "@esbuild/linux-x64": "0.21.5",
1528
+ "@esbuild/netbsd-x64": "0.21.5",
1529
+ "@esbuild/openbsd-x64": "0.21.5",
1530
+ "@esbuild/sunos-x64": "0.21.5",
1531
+ "@esbuild/win32-arm64": "0.21.5",
1532
+ "@esbuild/win32-ia32": "0.21.5",
1533
+ "@esbuild/win32-x64": "0.21.5"
1534
+ }
1535
+ },
1536
+ "node_modules/escalade": {
1537
+ "version": "3.2.0",
1538
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1539
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1540
+ "dev": true,
1541
+ "license": "MIT",
1542
+ "engines": {
1543
+ "node": ">=6"
1544
+ }
1545
+ },
1546
+ "node_modules/eventemitter3": {
1547
+ "version": "4.0.7",
1548
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
1549
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
1550
+ "license": "MIT"
1551
+ },
1552
+ "node_modules/fast-equals": {
1553
+ "version": "5.4.0",
1554
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
1555
+ "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
1556
+ "license": "MIT",
1557
+ "engines": {
1558
+ "node": ">=6.0.0"
1559
+ }
1560
+ },
1561
+ "node_modules/fsevents": {
1562
+ "version": "2.3.3",
1563
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1564
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1565
+ "dev": true,
1566
+ "hasInstallScript": true,
1567
+ "license": "MIT",
1568
+ "optional": true,
1569
+ "os": [
1570
+ "darwin"
1571
+ ],
1572
+ "engines": {
1573
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1574
+ }
1575
+ },
1576
+ "node_modules/gensync": {
1577
+ "version": "1.0.0-beta.2",
1578
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1579
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1580
+ "dev": true,
1581
+ "license": "MIT",
1582
+ "engines": {
1583
+ "node": ">=6.9.0"
1584
+ }
1585
+ },
1586
+ "node_modules/internmap": {
1587
+ "version": "2.0.3",
1588
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
1589
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
1590
+ "license": "ISC",
1591
+ "engines": {
1592
+ "node": ">=12"
1593
+ }
1594
+ },
1595
+ "node_modules/js-tokens": {
1596
+ "version": "4.0.0",
1597
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1598
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1599
+ "license": "MIT"
1600
+ },
1601
+ "node_modules/jsesc": {
1602
+ "version": "3.1.0",
1603
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1604
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1605
+ "dev": true,
1606
+ "license": "MIT",
1607
+ "bin": {
1608
+ "jsesc": "bin/jsesc"
1609
+ },
1610
+ "engines": {
1611
+ "node": ">=6"
1612
+ }
1613
+ },
1614
+ "node_modules/json5": {
1615
+ "version": "2.2.3",
1616
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1617
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1618
+ "dev": true,
1619
+ "license": "MIT",
1620
+ "bin": {
1621
+ "json5": "lib/cli.js"
1622
+ },
1623
+ "engines": {
1624
+ "node": ">=6"
1625
+ }
1626
+ },
1627
+ "node_modules/lodash": {
1628
+ "version": "4.18.1",
1629
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
1630
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
1631
+ "license": "MIT"
1632
+ },
1633
+ "node_modules/loose-envify": {
1634
+ "version": "1.4.0",
1635
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
1636
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
1637
+ "license": "MIT",
1638
+ "dependencies": {
1639
+ "js-tokens": "^3.0.0 || ^4.0.0"
1640
+ },
1641
+ "bin": {
1642
+ "loose-envify": "cli.js"
1643
+ }
1644
+ },
1645
+ "node_modules/lru-cache": {
1646
+ "version": "5.1.1",
1647
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1648
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1649
+ "dev": true,
1650
+ "license": "ISC",
1651
+ "dependencies": {
1652
+ "yallist": "^3.0.2"
1653
+ }
1654
+ },
1655
+ "node_modules/ms": {
1656
+ "version": "2.1.3",
1657
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1658
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1659
+ "dev": true,
1660
+ "license": "MIT"
1661
+ },
1662
+ "node_modules/nanoid": {
1663
+ "version": "3.3.12",
1664
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
1665
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
1666
+ "dev": true,
1667
+ "funding": [
1668
+ {
1669
+ "type": "github",
1670
+ "url": "https://github.com/sponsors/ai"
1671
+ }
1672
+ ],
1673
+ "license": "MIT",
1674
+ "bin": {
1675
+ "nanoid": "bin/nanoid.cjs"
1676
+ },
1677
+ "engines": {
1678
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1679
+ }
1680
+ },
1681
+ "node_modules/node-releases": {
1682
+ "version": "2.0.47",
1683
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
1684
+ "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
1685
+ "dev": true,
1686
+ "license": "MIT",
1687
+ "engines": {
1688
+ "node": ">=18"
1689
+ }
1690
+ },
1691
+ "node_modules/object-assign": {
1692
+ "version": "4.1.1",
1693
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1694
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1695
+ "license": "MIT",
1696
+ "engines": {
1697
+ "node": ">=0.10.0"
1698
+ }
1699
+ },
1700
+ "node_modules/picocolors": {
1701
+ "version": "1.1.1",
1702
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1703
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1704
+ "dev": true,
1705
+ "license": "ISC"
1706
+ },
1707
+ "node_modules/postcss": {
1708
+ "version": "8.5.15",
1709
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
1710
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
1711
+ "dev": true,
1712
+ "funding": [
1713
+ {
1714
+ "type": "opencollective",
1715
+ "url": "https://opencollective.com/postcss/"
1716
+ },
1717
+ {
1718
+ "type": "tidelift",
1719
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1720
+ },
1721
+ {
1722
+ "type": "github",
1723
+ "url": "https://github.com/sponsors/ai"
1724
+ }
1725
+ ],
1726
+ "license": "MIT",
1727
+ "dependencies": {
1728
+ "nanoid": "^3.3.12",
1729
+ "picocolors": "^1.1.1",
1730
+ "source-map-js": "^1.2.1"
1731
+ },
1732
+ "engines": {
1733
+ "node": "^10 || ^12 || >=14"
1734
+ }
1735
+ },
1736
+ "node_modules/prop-types": {
1737
+ "version": "15.8.1",
1738
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
1739
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
1740
+ "license": "MIT",
1741
+ "dependencies": {
1742
+ "loose-envify": "^1.4.0",
1743
+ "object-assign": "^4.1.1",
1744
+ "react-is": "^16.13.1"
1745
+ }
1746
+ },
1747
+ "node_modules/prop-types/node_modules/react-is": {
1748
+ "version": "16.13.1",
1749
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
1750
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
1751
+ "license": "MIT"
1752
+ },
1753
+ "node_modules/react": {
1754
+ "version": "18.3.1",
1755
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1756
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1757
+ "license": "MIT",
1758
+ "dependencies": {
1759
+ "loose-envify": "^1.1.0"
1760
+ },
1761
+ "engines": {
1762
+ "node": ">=0.10.0"
1763
+ }
1764
+ },
1765
+ "node_modules/react-dom": {
1766
+ "version": "18.3.1",
1767
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1768
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1769
+ "license": "MIT",
1770
+ "dependencies": {
1771
+ "loose-envify": "^1.1.0",
1772
+ "scheduler": "^0.23.2"
1773
+ },
1774
+ "peerDependencies": {
1775
+ "react": "^18.3.1"
1776
+ }
1777
+ },
1778
+ "node_modules/react-is": {
1779
+ "version": "18.3.1",
1780
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
1781
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
1782
+ "license": "MIT"
1783
+ },
1784
+ "node_modules/react-refresh": {
1785
+ "version": "0.17.0",
1786
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1787
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1788
+ "dev": true,
1789
+ "license": "MIT",
1790
+ "engines": {
1791
+ "node": ">=0.10.0"
1792
+ }
1793
+ },
1794
+ "node_modules/react-smooth": {
1795
+ "version": "4.0.4",
1796
+ "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
1797
+ "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
1798
+ "license": "MIT",
1799
+ "dependencies": {
1800
+ "fast-equals": "^5.0.1",
1801
+ "prop-types": "^15.8.1",
1802
+ "react-transition-group": "^4.4.5"
1803
+ },
1804
+ "peerDependencies": {
1805
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
1806
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
1807
+ }
1808
+ },
1809
+ "node_modules/react-transition-group": {
1810
+ "version": "4.4.5",
1811
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
1812
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
1813
+ "license": "BSD-3-Clause",
1814
+ "dependencies": {
1815
+ "@babel/runtime": "^7.5.5",
1816
+ "dom-helpers": "^5.0.1",
1817
+ "loose-envify": "^1.4.0",
1818
+ "prop-types": "^15.6.2"
1819
+ },
1820
+ "peerDependencies": {
1821
+ "react": ">=16.6.0",
1822
+ "react-dom": ">=16.6.0"
1823
+ }
1824
+ },
1825
+ "node_modules/recharts": {
1826
+ "version": "2.15.4",
1827
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
1828
+ "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==",
1829
+ "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide",
1830
+ "license": "MIT",
1831
+ "dependencies": {
1832
+ "clsx": "^2.0.0",
1833
+ "eventemitter3": "^4.0.1",
1834
+ "lodash": "^4.17.21",
1835
+ "react-is": "^18.3.1",
1836
+ "react-smooth": "^4.0.4",
1837
+ "recharts-scale": "^0.4.4",
1838
+ "tiny-invariant": "^1.3.1",
1839
+ "victory-vendor": "^36.6.8"
1840
+ },
1841
+ "engines": {
1842
+ "node": ">=14"
1843
+ },
1844
+ "peerDependencies": {
1845
+ "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
1846
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
1847
+ }
1848
+ },
1849
+ "node_modules/recharts-scale": {
1850
+ "version": "0.4.5",
1851
+ "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
1852
+ "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
1853
+ "license": "MIT",
1854
+ "dependencies": {
1855
+ "decimal.js-light": "^2.4.1"
1856
+ }
1857
+ },
1858
+ "node_modules/rollup": {
1859
+ "version": "4.62.0",
1860
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz",
1861
+ "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==",
1862
+ "dev": true,
1863
+ "license": "MIT",
1864
+ "dependencies": {
1865
+ "@types/estree": "1.0.9"
1866
+ },
1867
+ "bin": {
1868
+ "rollup": "dist/bin/rollup"
1869
+ },
1870
+ "engines": {
1871
+ "node": ">=18.0.0",
1872
+ "npm": ">=8.0.0"
1873
+ },
1874
+ "optionalDependencies": {
1875
+ "@rollup/rollup-android-arm-eabi": "4.62.0",
1876
+ "@rollup/rollup-android-arm64": "4.62.0",
1877
+ "@rollup/rollup-darwin-arm64": "4.62.0",
1878
+ "@rollup/rollup-darwin-x64": "4.62.0",
1879
+ "@rollup/rollup-freebsd-arm64": "4.62.0",
1880
+ "@rollup/rollup-freebsd-x64": "4.62.0",
1881
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.0",
1882
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.0",
1883
+ "@rollup/rollup-linux-arm64-gnu": "4.62.0",
1884
+ "@rollup/rollup-linux-arm64-musl": "4.62.0",
1885
+ "@rollup/rollup-linux-loong64-gnu": "4.62.0",
1886
+ "@rollup/rollup-linux-loong64-musl": "4.62.0",
1887
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.0",
1888
+ "@rollup/rollup-linux-ppc64-musl": "4.62.0",
1889
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.0",
1890
+ "@rollup/rollup-linux-riscv64-musl": "4.62.0",
1891
+ "@rollup/rollup-linux-s390x-gnu": "4.62.0",
1892
+ "@rollup/rollup-linux-x64-gnu": "4.62.0",
1893
+ "@rollup/rollup-linux-x64-musl": "4.62.0",
1894
+ "@rollup/rollup-openbsd-x64": "4.62.0",
1895
+ "@rollup/rollup-openharmony-arm64": "4.62.0",
1896
+ "@rollup/rollup-win32-arm64-msvc": "4.62.0",
1897
+ "@rollup/rollup-win32-ia32-msvc": "4.62.0",
1898
+ "@rollup/rollup-win32-x64-gnu": "4.62.0",
1899
+ "@rollup/rollup-win32-x64-msvc": "4.62.0",
1900
+ "fsevents": "~2.3.2"
1901
+ }
1902
+ },
1903
+ "node_modules/scheduler": {
1904
+ "version": "0.23.2",
1905
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1906
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1907
+ "license": "MIT",
1908
+ "dependencies": {
1909
+ "loose-envify": "^1.1.0"
1910
+ }
1911
+ },
1912
+ "node_modules/semver": {
1913
+ "version": "6.3.1",
1914
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1915
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1916
+ "dev": true,
1917
+ "license": "ISC",
1918
+ "bin": {
1919
+ "semver": "bin/semver.js"
1920
+ }
1921
+ },
1922
+ "node_modules/source-map-js": {
1923
+ "version": "1.2.1",
1924
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1925
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1926
+ "dev": true,
1927
+ "license": "BSD-3-Clause",
1928
+ "engines": {
1929
+ "node": ">=0.10.0"
1930
+ }
1931
+ },
1932
+ "node_modules/tiny-invariant": {
1933
+ "version": "1.3.3",
1934
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
1935
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
1936
+ "license": "MIT"
1937
+ },
1938
+ "node_modules/update-browserslist-db": {
1939
+ "version": "1.2.3",
1940
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
1941
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
1942
+ "dev": true,
1943
+ "funding": [
1944
+ {
1945
+ "type": "opencollective",
1946
+ "url": "https://opencollective.com/browserslist"
1947
+ },
1948
+ {
1949
+ "type": "tidelift",
1950
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1951
+ },
1952
+ {
1953
+ "type": "github",
1954
+ "url": "https://github.com/sponsors/ai"
1955
+ }
1956
+ ],
1957
+ "license": "MIT",
1958
+ "dependencies": {
1959
+ "escalade": "^3.2.0",
1960
+ "picocolors": "^1.1.1"
1961
+ },
1962
+ "bin": {
1963
+ "update-browserslist-db": "cli.js"
1964
+ },
1965
+ "peerDependencies": {
1966
+ "browserslist": ">= 4.21.0"
1967
+ }
1968
+ },
1969
+ "node_modules/victory-vendor": {
1970
+ "version": "36.9.2",
1971
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
1972
+ "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
1973
+ "license": "MIT AND ISC",
1974
+ "dependencies": {
1975
+ "@types/d3-array": "^3.0.3",
1976
+ "@types/d3-ease": "^3.0.0",
1977
+ "@types/d3-interpolate": "^3.0.1",
1978
+ "@types/d3-scale": "^4.0.2",
1979
+ "@types/d3-shape": "^3.1.0",
1980
+ "@types/d3-time": "^3.0.0",
1981
+ "@types/d3-timer": "^3.0.0",
1982
+ "d3-array": "^3.1.6",
1983
+ "d3-ease": "^3.0.1",
1984
+ "d3-interpolate": "^3.0.1",
1985
+ "d3-scale": "^4.0.2",
1986
+ "d3-shape": "^3.1.0",
1987
+ "d3-time": "^3.0.0",
1988
+ "d3-timer": "^3.0.1"
1989
+ }
1990
+ },
1991
+ "node_modules/vite": {
1992
+ "version": "5.4.21",
1993
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
1994
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
1995
+ "dev": true,
1996
+ "license": "MIT",
1997
+ "dependencies": {
1998
+ "esbuild": "^0.21.3",
1999
+ "postcss": "^8.4.43",
2000
+ "rollup": "^4.20.0"
2001
+ },
2002
+ "bin": {
2003
+ "vite": "bin/vite.js"
2004
+ },
2005
+ "engines": {
2006
+ "node": "^18.0.0 || >=20.0.0"
2007
+ },
2008
+ "funding": {
2009
+ "url": "https://github.com/vitejs/vite?sponsor=1"
2010
+ },
2011
+ "optionalDependencies": {
2012
+ "fsevents": "~2.3.3"
2013
+ },
2014
+ "peerDependencies": {
2015
+ "@types/node": "^18.0.0 || >=20.0.0",
2016
+ "less": "*",
2017
+ "lightningcss": "^1.21.0",
2018
+ "sass": "*",
2019
+ "sass-embedded": "*",
2020
+ "stylus": "*",
2021
+ "sugarss": "*",
2022
+ "terser": "^5.4.0"
2023
+ },
2024
+ "peerDependenciesMeta": {
2025
+ "@types/node": {
2026
+ "optional": true
2027
+ },
2028
+ "less": {
2029
+ "optional": true
2030
+ },
2031
+ "lightningcss": {
2032
+ "optional": true
2033
+ },
2034
+ "sass": {
2035
+ "optional": true
2036
+ },
2037
+ "sass-embedded": {
2038
+ "optional": true
2039
+ },
2040
+ "stylus": {
2041
+ "optional": true
2042
+ },
2043
+ "sugarss": {
2044
+ "optional": true
2045
+ },
2046
+ "terser": {
2047
+ "optional": true
2048
+ }
2049
+ }
2050
+ },
2051
+ "node_modules/yallist": {
2052
+ "version": "3.1.1",
2053
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
2054
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
2055
+ "dev": true,
2056
+ "license": "ISC"
2057
+ }
2058
+ }
2059
+ }
frontend/package.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "augur-frontend",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.3.1",
13
+ "react-dom": "^18.3.1",
14
+ "recharts": "^2.12.7"
15
+ },
16
+ "devDependencies": {
17
+ "@vitejs/plugin-react": "^4.3.1",
18
+ "vite": "^5.4.0"
19
+ }
20
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useMemo, useEffect } from "react";
2
+ import {
3
+ ResponsiveContainer,
4
+ AreaChart,
5
+ Area,
6
+ XAxis,
7
+ YAxis,
8
+ CartesianGrid,
9
+ ReferenceLine,
10
+ ReferenceArea,
11
+ Tooltip,
12
+ } from "recharts";
13
+
14
+ const API_URL = import.meta.env.VITE_API_URL || "/api/predict";
15
+
16
+ /* Extract contiguous anomaly regions [start, end] from the frame list.
17
+ These become the glowing alarm bands behind the trace. */
18
+ function anomalyBands(frames) {
19
+ const bands = [];
20
+ let start = null;
21
+ for (const f of frames) {
22
+ if (f.is_anomaly) {
23
+ if (start === null) start = f.frame_idx;
24
+ } else if (start !== null) {
25
+ bands.push([start, f.frame_idx - 1]);
26
+ start = null;
27
+ }
28
+ }
29
+ if (start !== null) bands.push([start, frames[frames.length - 1].frame_idx]);
30
+ return bands;
31
+ }
32
+
33
+ /* Tooltip shows the threshold-relative value (intuitive) AND the raw MSE
34
+ score (technical transparency). */
35
+ function makeTip(threshold) {
36
+ return function TipBox({ active, payload }) {
37
+ if (!active || !payload || !payload.length) return null;
38
+ const p = payload[0].payload;
39
+ if (p.rawScore === null || p.rawScore === undefined) return null;
40
+ const rel = p.rawScore / threshold;
41
+ return (
42
+ <div className="tip">
43
+ <div className="row"><span className="lab">FRAME</span><span className="val">{p.frame}</span></div>
44
+ <div className="row">
45
+ <span className="lab">SURPRISE</span>
46
+ <span className={"val" + (p.is_anomaly ? " alarm" : "")}>{rel.toFixed(2)}×</span>
47
+ </div>
48
+ <div className="row">
49
+ <span className="lab">RAW</span>
50
+ <span className="val">{p.rawScore.toExponential(2)}</span>
51
+ </div>
52
+ </div>
53
+ );
54
+ };
55
+ }
56
+
57
+ export default function App() {
58
+ const [file, setFile] = useState(null);
59
+ const [loading, setLoading] = useState(false);
60
+ const [result, setResult] = useState(null);
61
+ const [error, setError] = useState(null);
62
+ const [drag, setDrag] = useState(false);
63
+ const [currentFrame, setCurrentFrame] = useState(0);
64
+ const inputRef = useRef(null);
65
+ const videoRef = useRef(null);
66
+
67
+ // Playable URL for the uploaded file (revoke on change to avoid leaks)
68
+ const videoUrl = useMemo(() => (file ? URL.createObjectURL(file) : null), [file]);
69
+ useEffect(() => {
70
+ return () => { if (videoUrl) URL.revokeObjectURL(videoUrl); };
71
+ }, [videoUrl]);
72
+
73
+ const status = loading ? "reading" : result ? "flagged" : "standby";
74
+ const statusLabel = loading ? "READING FEED" : result ? "ANALYSIS COMPLETE" : "FEED STANDBY";
75
+
76
+ function pick(f) {
77
+ if (!f) return;
78
+ setFile(f);
79
+ setResult(null);
80
+ setError(null);
81
+ setCurrentFrame(0);
82
+ }
83
+
84
+ async function analyze() {
85
+ if (!file) return;
86
+ setLoading(true);
87
+ setError(null);
88
+ setResult(null);
89
+ setCurrentFrame(0);
90
+ try {
91
+ const body = new FormData();
92
+ body.append("file", file);
93
+ const res = await fetch(API_URL, { method: "POST", body });
94
+ if (!res.ok) {
95
+ const detail = await res.json().catch(() => ({}));
96
+ throw new Error(detail.detail || `Server returned ${res.status}`);
97
+ }
98
+ setResult(await res.json());
99
+ } catch (e) {
100
+ setError(
101
+ e.message?.includes("fetch")
102
+ ? "Cannot reach the detector. Start the backend, then run again."
103
+ : e.message
104
+ );
105
+ } finally {
106
+ setLoading(false);
107
+ }
108
+ }
109
+
110
+ const fps = result?.fps || 10;
111
+ const threshold = result?.threshold || 1;
112
+
113
+ // Trace data: store BOTH the threshold-relative value (for the axis) and the
114
+ // raw score (for the tooltip). Tripwire sits at 1.0x.
115
+ const chartData = useMemo(
116
+ () =>
117
+ result?.frames.map((f) => ({
118
+ frame: f.frame_idx,
119
+ rel: f.score === null ? null : f.score / threshold,
120
+ rawScore: f.score,
121
+ is_anomaly: f.is_anomaly,
122
+ })) ?? [],
123
+ [result, threshold]
124
+ );
125
+
126
+ // Progressive reveal: hide values beyond the playhead so the trace draws in sync
127
+ const revealedData = useMemo(
128
+ () => chartData.map((d) => ({ ...d, rel: d.frame <= currentFrame ? d.rel : null })),
129
+ [chartData, currentFrame]
130
+ );
131
+
132
+ const bands = useMemo(() => (result ? anomalyBands(result.frames) : []), [result]);
133
+ const peakRel = useMemo(() => {
134
+ const s = result?.frames.map((f) => f.score).filter((v) => v !== null) ?? [];
135
+ return s.length ? Math.max(...s) / threshold : 0;
136
+ }, [result, threshold]);
137
+
138
+ const liveFrame = result?.frames[currentFrame];
139
+ const liveRel = liveFrame && liveFrame.score !== null ? liveFrame.score / threshold : null;
140
+
141
+ const TipBox = useMemo(() => makeTip(threshold), [threshold]);
142
+
143
+ return (
144
+ <div className="shell">
145
+ <header className="masthead">
146
+ <div>
147
+ <div className="wordmark">AUGUR</div>
148
+ <div className="tagline">Every frame, predicted. Every surprise, flagged.</div>
149
+ </div>
150
+ <div className="status" data-state={status}>
151
+ <span className="dot" />
152
+ {statusLabel}
153
+ </div>
154
+ </header>
155
+
156
+ <section className="intake">
157
+ <div
158
+ className="dropzone"
159
+ data-drag={drag}
160
+ onClick={() => inputRef.current?.click()}
161
+ onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
162
+ onDragLeave={() => setDrag(false)}
163
+ onDrop={(e) => { e.preventDefault(); setDrag(false); pick(e.dataTransfer.files?.[0]); }}
164
+ >
165
+ <div className="eyebrow">VIDEO FEED INPUT</div>
166
+ <div className="prompt">Drop a video, or click to select</div>
167
+ <div className="sub">The detector learns normal motion, then flags what it cannot predict.</div>
168
+ {file && <div className="filename">{file.name}</div>}
169
+ <input
170
+ ref={inputRef}
171
+ type="file"
172
+ accept="video/*"
173
+ className="hidden-input"
174
+ onChange={(e) => pick(e.target.files?.[0])}
175
+ />
176
+ </div>
177
+
178
+ <button className="run" onClick={analyze} disabled={!file || loading}>
179
+ {loading ? "ANALYZING…" : "RUN DETECTION"}
180
+ </button>
181
+
182
+ {error && <div className="error">{error}</div>}
183
+ </section>
184
+
185
+ {result && (
186
+ <section className="stats">
187
+ <div className="stat">
188
+ <div className="k">FRAMES</div>
189
+ <div className="v">{result.total_frames}</div>
190
+ </div>
191
+ <div className="stat">
192
+ <div className="k">FLAGGED</div>
193
+ <div className="v alarm">{result.frames.filter((f) => f.is_anomaly).length}</div>
194
+ </div>
195
+ <div className="stat">
196
+ <div className="k">PEAK SURPRISE</div>
197
+ <div className="v">{peakRel.toFixed(2)}×</div>
198
+ </div>
199
+ <div className="stat">
200
+ <div className="k">ALARM LINE</div>
201
+ <div className="v amber">1.00×</div>
202
+ </div>
203
+ </section>
204
+ )}
205
+
206
+ {/* Synced playback: video + live readout */}
207
+ {result && videoUrl && (
208
+ <section className="playback">
209
+ <video
210
+ ref={videoRef}
211
+ src={videoUrl}
212
+ controls
213
+ className="feed-video"
214
+ onTimeUpdate={(e) => setCurrentFrame(Math.floor(e.target.currentTime * fps))}
215
+ />
216
+ <div className="live-readout">
217
+ <span className="lr-frame">FRAME {currentFrame}</span>
218
+ {!liveFrame || liveFrame.score === null ? (
219
+ <span className="lr-warm">WARMING UP</span>
220
+ ) : (
221
+ <span className={"lr-score" + (liveFrame.is_anomaly ? " alarm" : "")}>
222
+ {liveRel.toFixed(2)}× {liveFrame.is_anomaly ? "· ANOMALY" : "· normal"}
223
+ </span>
224
+ )}
225
+ </div>
226
+ </section>
227
+ )}
228
+
229
+ <section className="trace">
230
+ <div className="trace-head">
231
+ <div className="trace-title">THE SURPRISE TRACE</div>
232
+ <div className="trace-legend">
233
+ <span className="item"><span className="swatch calm" />signal</span>
234
+ <span className="item"><span className="swatch amber" />tripwire</span>
235
+ <span className="item"><span className="swatch alarm" />anomaly</span>
236
+ </div>
237
+ </div>
238
+ <div className="trace-note">
239
+ Surprise shown relative to the detection threshold — 1.0× is the alarm line.
240
+ </div>
241
+
242
+ {result ? (
243
+ <ResponsiveContainer width="100%" height={300}>
244
+ <AreaChart data={revealedData} margin={{ top: 8, right: 12, bottom: 4, left: 0 }}>
245
+ <defs>
246
+ <linearGradient id="calmFill" x1="0" y1="0" x2="0" y2="1">
247
+ <stop offset="0%" stopColor="#56C7BE" stopOpacity={0.28} />
248
+ <stop offset="100%" stopColor="#56C7BE" stopOpacity={0} />
249
+ </linearGradient>
250
+ </defs>
251
+
252
+ <CartesianGrid strokeDasharray="2 4" vertical={false} />
253
+
254
+ {/* glowing alarm bands behind the trace (full range, not revealed) */}
255
+ {bands.map(([a, b], i) => (
256
+ <ReferenceArea key={i} x1={a} x2={b} fill="#FF6A5A" fillOpacity={0.10} stroke="none" />
257
+ ))}
258
+
259
+ <XAxis dataKey="frame" type="number" domain={[0, result.total_frames]}
260
+ tickLine={false} interval="preserveStartEnd" minTickGap={40} allowDataOverflow />
261
+ <YAxis tickFormatter={(v) => `${v.toFixed(1)}×`} width={48} tickLine={false} />
262
+
263
+ {/* the amber tripwire — now fixed at 1.0x */}
264
+ <ReferenceLine
265
+ y={1.0}
266
+ stroke="#E6A93C"
267
+ strokeDasharray="5 4"
268
+ strokeWidth={1.2}
269
+ label={{ value: "TRIPWIRE", position: "insideTopRight", fill: "#E6A93C", fontSize: 10, fontFamily: "JetBrains Mono" }}
270
+ />
271
+
272
+ {/* the playhead — follows the video */}
273
+ <ReferenceLine x={currentFrame} stroke="#56C7BE" strokeWidth={1.5} strokeOpacity={0.9} />
274
+
275
+ <Tooltip content={<TipBox />} cursor={{ stroke: "#66768A", strokeDasharray: "3 3" }} />
276
+
277
+ {/* connectNulls=false leaves a gap over warm-up AND beyond the playhead */}
278
+ <Area
279
+ type="monotone"
280
+ dataKey="rel"
281
+ stroke="#56C7BE"
282
+ strokeWidth={1.6}
283
+ fill="url(#calmFill)"
284
+ connectNulls={false}
285
+ isAnimationActive={false}
286
+ dot={false}
287
+ activeDot={{ r: 3, fill: "#56C7BE", stroke: "none" }}
288
+ />
289
+ </AreaChart>
290
+ </ResponsiveContainer>
291
+ ) : (
292
+ <div className="idle">
293
+ <div className="label">{loading ? "READING FEED…" : "AWAITING FEED"}</div>
294
+ <div className="scan" />
295
+ <div className="label" style={{ color: "var(--dim-2)", fontSize: 11 }}>
296
+ SURPRISE / FRAME
297
+ </div>
298
+ </div>
299
+ )}
300
+ </section>
301
+
302
+ {/* Most anomalous moments — heatmap overlays */}
303
+ {result && result.top_anomalies?.length > 0 && (
304
+ <section className="moments">
305
+ <div className="moments-head">
306
+ <div className="moments-title">MOST ANOMALOUS MOMENTS</div>
307
+ <div className="moments-sub">Where the model was most surprised — heatmap over frame</div>
308
+ </div>
309
+ <div className="moments-grid">
310
+ {result.top_anomalies.map((a) => (
311
+ <figure className="moment" key={a.frame_idx}>
312
+ <img
313
+ className="moment-img"
314
+ src={`data:image/png;base64,${a.overlay}`}
315
+ alt={`Frame ${a.frame_idx}`}
316
+ />
317
+ <figcaption className="moment-cap">
318
+ <span className="moment-frame">FRAME {a.frame_idx}</span>
319
+ <span className="moment-score">{(a.score / threshold).toFixed(2)}×</span>
320
+ </figcaption>
321
+ </figure>
322
+ ))}
323
+ </div>
324
+ </section>
325
+ )}
326
+ </div>
327
+ );
328
+ }
frontend/src/index.css ADDED
@@ -0,0 +1,410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================
2
+ AUGUR — design system
3
+ A surveillance-instrument console for predictive anomaly
4
+ detection. The signal trace is the hero; everything else
5
+ stays quiet. Color encodes meaning: teal = calm/normal,
6
+ coral = alarm/anomaly, amber = the threshold tripwire.
7
+ ============================================================ */
8
+
9
+ :root {
10
+ --void: #0A0E13; /* cold monitor black */
11
+ --panel: #121922; /* raised surface */
12
+ --panel-2: #0E141B; /* recessed surface */
13
+ --line: #1F2A35; /* hairline grid / dividers */
14
+ --line-soft: #18222C;
15
+
16
+ --calm: #56C7BE; /* normal signal — all clear */
17
+ --calm-dim: #2E6B66;
18
+ --alarm: #FF6A5A; /* anomaly — surprise crosses the line */
19
+ --amber: #E6A93C; /* sodium-lamp threshold tripwire */
20
+
21
+ --text: #C8D4DF;
22
+ --dim: #66768A;
23
+ --dim-2: #44515F;
24
+
25
+ --font-display: "Space Grotesk", system-ui, sans-serif;
26
+ --font-mono: "JetBrains Mono", ui-monospace, monospace;
27
+ }
28
+
29
+ * { box-sizing: border-box; margin: 0; padding: 0; }
30
+
31
+ html, body, #root { height: 100%; }
32
+
33
+ body {
34
+ background: var(--void);
35
+ color: var(--text);
36
+ font-family: var(--font-display);
37
+ -webkit-font-smoothing: antialiased;
38
+ /* faint cold vignette so the console feels lit from the center */
39
+ background-image: radial-gradient(120% 90% at 50% -10%, #0F1722 0%, var(--void) 60%);
40
+ }
41
+
42
+ /* ---- shell ---------------------------------------------------- */
43
+
44
+ .shell {
45
+ max-width: 1080px;
46
+ margin: 0 auto;
47
+ padding: 40px 28px 80px;
48
+ }
49
+
50
+ /* ---- masthead ------------------------------------------------- */
51
+
52
+ .masthead {
53
+ display: flex;
54
+ align-items: flex-start;
55
+ justify-content: space-between;
56
+ gap: 24px;
57
+ padding-bottom: 28px;
58
+ border-bottom: 1px solid var(--line);
59
+ }
60
+
61
+ .wordmark {
62
+ font-family: var(--font-display);
63
+ font-weight: 700;
64
+ font-size: 28px;
65
+ letter-spacing: 0.42em;
66
+ text-indent: 0.42em; /* compensate so it stays optically centered */
67
+ color: var(--text);
68
+ }
69
+
70
+ .tagline {
71
+ margin-top: 10px;
72
+ font-family: var(--font-mono);
73
+ font-size: 13px;
74
+ letter-spacing: 0.02em;
75
+ color: var(--dim);
76
+ }
77
+
78
+ /* status chip — live instrument indicator */
79
+ .status {
80
+ display: inline-flex;
81
+ align-items: center;
82
+ gap: 9px;
83
+ font-family: var(--font-mono);
84
+ font-size: 11px;
85
+ letter-spacing: 0.18em;
86
+ color: var(--dim);
87
+ padding: 8px 13px;
88
+ border: 1px solid var(--line);
89
+ border-radius: 2px;
90
+ background: var(--panel-2);
91
+ white-space: nowrap;
92
+ }
93
+ .status .dot {
94
+ width: 7px; height: 7px; border-radius: 50%;
95
+ background: var(--dim-2);
96
+ }
97
+ .status[data-state="standby"] .dot { background: var(--amber); animation: blink 2.4s ease-in-out infinite; }
98
+ .status[data-state="reading"] .dot { background: var(--calm); animation: blink 0.7s ease-in-out infinite; }
99
+ .status[data-state="flagged"] .dot { background: var(--alarm); box-shadow: 0 0 10px var(--alarm); }
100
+
101
+ @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.25} }
102
+
103
+ /* ---- drop zone ------------------------------------------------ */
104
+
105
+ .intake {
106
+ margin-top: 36px;
107
+ }
108
+
109
+ .dropzone {
110
+ position: relative;
111
+ border: 1px dashed var(--line);
112
+ border-radius: 3px;
113
+ background:
114
+ repeating-linear-gradient(45deg, transparent 0 11px, rgba(86,199,190,0.015) 11px 12px),
115
+ var(--panel-2);
116
+ padding: 54px 32px;
117
+ text-align: center;
118
+ cursor: pointer;
119
+ transition: border-color 0.18s ease, background-color 0.18s ease;
120
+ }
121
+ .dropzone:hover { border-color: var(--calm-dim); }
122
+ .dropzone[data-drag="true"] {
123
+ border-color: var(--calm);
124
+ background-color: rgba(86,199,190,0.04);
125
+ }
126
+
127
+ .dropzone .eyebrow {
128
+ font-family: var(--font-mono);
129
+ font-size: 11px;
130
+ letter-spacing: 0.24em;
131
+ color: var(--dim);
132
+ margin-bottom: 14px;
133
+ }
134
+ .dropzone .prompt {
135
+ font-family: var(--font-display);
136
+ font-weight: 500;
137
+ font-size: 20px;
138
+ color: var(--text);
139
+ margin-bottom: 8px;
140
+ }
141
+ .dropzone .sub {
142
+ font-family: var(--font-mono);
143
+ font-size: 12px;
144
+ color: var(--dim-2);
145
+ }
146
+ .dropzone .filename {
147
+ margin-top: 16px;
148
+ font-family: var(--font-mono);
149
+ font-size: 13px;
150
+ color: var(--calm);
151
+ }
152
+ .hidden-input { display: none; }
153
+
154
+ .run {
155
+ margin-top: 18px;
156
+ display: inline-flex;
157
+ align-items: center;
158
+ gap: 10px;
159
+ font-family: var(--font-mono);
160
+ font-size: 13px;
161
+ letter-spacing: 0.12em;
162
+ color: var(--void);
163
+ background: var(--calm);
164
+ border: none;
165
+ border-radius: 2px;
166
+ padding: 13px 26px;
167
+ cursor: pointer;
168
+ transition: filter 0.15s ease, opacity 0.15s ease;
169
+ }
170
+ .run:hover { filter: brightness(1.08); }
171
+ .run:disabled { opacity: 0.4; cursor: not-allowed; }
172
+
173
+ .error {
174
+ margin-top: 16px;
175
+ font-family: var(--font-mono);
176
+ font-size: 13px;
177
+ color: var(--alarm);
178
+ border-left: 2px solid var(--alarm);
179
+ padding-left: 12px;
180
+ }
181
+
182
+ /* ---- stat strip ---------------------------------------------- */
183
+
184
+ .stats {
185
+ margin-top: 44px;
186
+ display: grid;
187
+ grid-template-columns: repeat(4, 1fr);
188
+ border: 1px solid var(--line);
189
+ border-radius: 3px;
190
+ overflow: hidden;
191
+ background: var(--panel);
192
+ }
193
+ .stat {
194
+ padding: 20px 22px;
195
+ border-right: 1px solid var(--line);
196
+ }
197
+ .stat:last-child { border-right: none; }
198
+ .stat .k {
199
+ font-family: var(--font-mono);
200
+ font-size: 10px;
201
+ letter-spacing: 0.22em;
202
+ color: var(--dim);
203
+ margin-bottom: 10px;
204
+ }
205
+ .stat .v {
206
+ font-family: var(--font-mono);
207
+ font-weight: 700;
208
+ font-size: 26px;
209
+ color: var(--text);
210
+ line-height: 1;
211
+ }
212
+ .stat .v.alarm { color: var(--alarm); }
213
+ .stat .v.amber { color: var(--amber); }
214
+
215
+ /* ---- trace panel (the signature) ------------------------------ */
216
+
217
+ .trace {
218
+ margin-top: 18px;
219
+ border: 1px solid var(--line);
220
+ border-radius: 3px;
221
+ background:
222
+ linear-gradient(180deg, rgba(86,199,190,0.02), transparent 30%),
223
+ var(--panel);
224
+ padding: 22px 20px 14px;
225
+ }
226
+ .trace-head {
227
+ display: flex;
228
+ align-items: baseline;
229
+ justify-content: space-between;
230
+ margin-bottom: 18px;
231
+ padding: 0 6px;
232
+ }
233
+ .trace-title {
234
+ font-family: var(--font-display);
235
+ font-weight: 700;
236
+ font-size: 15px;
237
+ letter-spacing: 0.16em;
238
+ color: var(--text);
239
+ }
240
+ .trace-legend {
241
+ display: flex;
242
+ gap: 18px;
243
+ font-family: var(--font-mono);
244
+ font-size: 11px;
245
+ color: var(--dim);
246
+ }
247
+ .trace-legend .item { display: inline-flex; align-items: center; gap: 7px; }
248
+ .trace-legend .swatch { width: 14px; height: 2px; display: inline-block; }
249
+ .swatch.calm { background: var(--calm); }
250
+ .swatch.amber { background: var(--amber); }
251
+ .swatch.alarm { background: var(--alarm); height: 8px; opacity: 0.55; }
252
+
253
+ /* idle ambient trace (empty state) */
254
+ .idle {
255
+ height: 280px;
256
+ display: flex;
257
+ flex-direction: column;
258
+ align-items: center;
259
+ justify-content: center;
260
+ gap: 18px;
261
+ }
262
+ .idle .scan {
263
+ width: 100%;
264
+ height: 1px;
265
+ background: linear-gradient(90deg, transparent, var(--calm-dim), transparent);
266
+ opacity: 0.5;
267
+ animation: sweep 3.4s ease-in-out infinite;
268
+ }
269
+ @keyframes sweep { 0%,100%{transform:scaleX(0.2);opacity:0.2} 50%{transform:scaleX(1);opacity:0.55} }
270
+ .idle .label {
271
+ font-family: var(--font-mono);
272
+ font-size: 12px;
273
+ letter-spacing: 0.2em;
274
+ color: var(--dim-2);
275
+ }
276
+
277
+ /* recharts overrides — keep the instrument quiet */
278
+ .recharts-cartesian-grid line { stroke: var(--line-soft); }
279
+ .recharts-text { fill: var(--dim); font-family: var(--font-mono); font-size: 11px; }
280
+ .recharts-cartesian-axis-line { stroke: var(--line); }
281
+
282
+ /* custom tooltip */
283
+ .tip {
284
+ background: var(--void);
285
+ border: 1px solid var(--line);
286
+ border-radius: 2px;
287
+ padding: 10px 12px;
288
+ font-family: var(--font-mono);
289
+ font-size: 12px;
290
+ }
291
+ .tip .row { display: flex; gap: 14px; justify-content: space-between; }
292
+ .tip .lab { color: var(--dim); }
293
+ .tip .val { color: var(--text); }
294
+ .tip .val.alarm { color: var(--alarm); }
295
+
296
+ /* ---- responsive ----------------------------------------------- */
297
+
298
+ @media (max-width: 680px) {
299
+ .masthead { flex-direction: column; }
300
+ .stats { grid-template-columns: repeat(2, 1fr); }
301
+ .stat:nth-child(2) { border-right: none; }
302
+ .stat:nth-child(1), .stat:nth-child(2) { border-bottom: 1px solid var(--line); }
303
+ .wordmark { font-size: 22px; letter-spacing: 0.32em; }
304
+ }
305
+
306
+ @media (prefers-reduced-motion: reduce) {
307
+ .status .dot, .idle .scan { animation: none; }
308
+ }
309
+
310
+ /* ---- anomalous moments strip ---------------------------------- */
311
+
312
+ .moments { margin-top: 18px; }
313
+
314
+ .moments-head {
315
+ display: flex;
316
+ align-items: baseline;
317
+ justify-content: space-between;
318
+ margin-bottom: 14px;
319
+ padding: 0 2px;
320
+ }
321
+ .moments-title {
322
+ font-family: var(--font-display);
323
+ font-weight: 700;
324
+ font-size: 14px;
325
+ letter-spacing: 0.16em;
326
+ color: var(--text);
327
+ }
328
+ .moments-sub {
329
+ font-family: var(--font-mono);
330
+ font-size: 11px;
331
+ color: var(--dim);
332
+ }
333
+
334
+ .moments-grid {
335
+ display: grid;
336
+ grid-template-columns: repeat(5, 1fr);
337
+ gap: 12px;
338
+ }
339
+
340
+ .moment {
341
+ border: 1px solid var(--line);
342
+ border-radius: 3px;
343
+ overflow: hidden;
344
+ background: var(--panel);
345
+ transition: border-color 0.15s ease, transform 0.15s ease;
346
+ }
347
+ .moment:hover {
348
+ border-color: var(--alarm);
349
+ transform: translateY(-2px);
350
+ }
351
+ .moment-img {
352
+ display: block;
353
+ width: 100%;
354
+ aspect-ratio: 1;
355
+ object-fit: cover;
356
+ image-rendering: pixelated; /* 128x128 büyürken keskin kalsın */
357
+ }
358
+ .moment-cap {
359
+ display: flex;
360
+ align-items: center;
361
+ justify-content: space-between;
362
+ padding: 9px 11px;
363
+ font-family: var(--font-mono);
364
+ font-size: 11px;
365
+ border-top: 1px solid var(--line);
366
+ }
367
+ .moment-frame { color: var(--dim); letter-spacing: 0.08em; }
368
+ .moment-score { color: var(--alarm); font-weight: 500; }
369
+
370
+ @media (max-width: 680px) {
371
+ .moments-grid { grid-template-columns: repeat(2, 1fr); }
372
+ .moments-head { flex-direction: column; gap: 4px; }
373
+ }
374
+
375
+ /* ---- synced playback -------------------- */
376
+
377
+ .playback { margin-top: 18px; }
378
+
379
+ .feed-video {
380
+ width: 100%;
381
+ max-height: 360px;
382
+ border: 1px solid var(--line);
383
+ border-radius: 3px;
384
+ background: #000;
385
+ display: block;
386
+ }
387
+
388
+ .live-readout {
389
+ display: flex;
390
+ gap: 18px;
391
+ align-items: baseline;
392
+ margin-top: 10px;
393
+ padding: 0 2px;
394
+ font-family: var(--font-mono);
395
+ font-size: 13px;
396
+ }
397
+ .lr-frame { color: var(--dim); letter-spacing: 0.08em; }
398
+ .lr-score { color: var(--calm); }
399
+ .lr-score.alarm { color: var(--alarm); font-weight: 700; }
400
+ .lr-warm { color: var(--dim-2); letter-spacing: 0.12em; }
401
+
402
+ /* ---- trace explanation note (append to index.css) ------------- */
403
+
404
+ .trace-note {
405
+ font-family: var(--font-mono);
406
+ font-size: 11px;
407
+ color: var(--dim);
408
+ padding: 0 6px 14px;
409
+ margin-top: -6px;
410
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import App from "./App.jsx";
4
+ import "./index.css";
5
+
6
+ ReactDOM.createRoot(document.getElementById("root")).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );
frontend/vite.config.js ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173,
8
+ proxy: {
9
+ "/api": {
10
+ target: "http://localhost:8000",
11
+ changeOrigin: true,
12
+ rewrite: (path) => path.replace(/^\/api/, ""),
13
+ },
14
+ },
15
+ },
16
+ });
requirements-inference.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Inference-only dependencies (CPU serving image)
2
+ numpy
3
+ opencv-python-headless
4
+ pillow
5
+ onnxruntime
6
+ fastapi
7
+ uvicorn[standard]
8
+ python-multipart
9
+ matplotlib
10
+ torch
11
+ torchvision
12
+ prometheus-fastapi-instrumentator
src/__init__.py ADDED
File without changes
src/data/__init__.py ADDED
File without changes
src/data/shanghai_loader.py ADDED
File without changes
src/data/ucsd_loader.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module for dataset and dataloaders of UCSD dataset.
3
+ """
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import List, Tuple, Optional
8
+ import numpy as np
9
+ from PIL import Image
10
+ import torch
11
+ from torch.utils.data import Dataset
12
+
13
+ from src.data.video_transforms import transform
14
+
15
+
16
+ class UCSDDataset(Dataset):
17
+ """
18
+ UCSD Anomaly Detection Dataset.
19
+
20
+ Train: only normal clips.
21
+ Test: clips with frame-level ground truth annotations.
22
+
23
+ Args:
24
+ root: Dataset root path (containing UCSDped1/, UCSDped2/)
25
+ subset: 'Ped1' or 'Ped2'
26
+ split: 'train' or 'test'
27
+ window_size: Number of frames per sample (sliding window)
28
+ stride: Stride between windows
29
+ transform: Optional transform applied to each frame
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ root: str,
35
+ subset: str = "Ped2",
36
+ split: str = "train",
37
+ window_size: int = 16,
38
+ stride: int = 8,
39
+ mode: str = "reconstruction",
40
+ transform: Optional[callable] = None,
41
+ clip_indices: Optional[List[int]] = None
42
+ ):
43
+ super().__init__()
44
+ self.root = Path(root)
45
+ self.subset = subset.lower()
46
+ self.split = split
47
+ self.window_size = window_size
48
+ self.stride = stride
49
+ self.mode = mode
50
+ self.transform = transform
51
+ self.clip_indices = clip_indices
52
+
53
+ # Subset check
54
+ assert subset in ("ped1", "ped2"), f"subset must be ped1 or ped2, got {subset}"
55
+
56
+ # Read the subset and store the clip directories
57
+ self.subset_split = self.root / f"UCSD{self.subset}" / f"{split.title()}"
58
+
59
+ # Sanity check to ensure the files and clip directories exist
60
+ if not self.subset_split.exists():
61
+ raise FileNotFoundError(f"Dataset path not found: {self.subset_split}")
62
+
63
+ self.clip_dirs = sorted([
64
+ d for d in self.subset_split.iterdir()
65
+ if d.is_dir() and not d.name.endswith("_gt")
66
+ ])
67
+
68
+ # Filter out the clips
69
+ if self.clip_indices is not None:
70
+ self.clip_dirs = [self.clip_dirs[i] for i in self.clip_indices]
71
+
72
+ if len(self.clip_dirs) == 0:
73
+ raise RuntimeError(f"No clip directories found in {self.subset_split}")
74
+
75
+ # Collect the clip paths
76
+ self.clips = []
77
+ for clip_dir in self.clip_dirs: # clip_dir = Path("Train001")
78
+ frame_paths = sorted(clip_dir.glob("*.tif")) # liste of frame paths
79
+ frames = np.stack([np.array(Image.open(p)) for p in frame_paths])
80
+ self.clips.append(frames)
81
+
82
+ # Create labels based on split
83
+ if self.split == "test":
84
+ m_file = self.subset_split / f"UCSD{subset}.m" # path case dikkat
85
+ content = m_file.read_text()
86
+ matches = re.findall(r"\[(\d+):(\d+)\]", content)
87
+
88
+ self.labels = []
89
+ for clip_idx, (start_str, end_str) in enumerate(matches):
90
+ start, end = int(start_str), int(end_str)
91
+ n_frames = len(self.clips[clip_idx]) # clip's frame length
92
+ label = np.zeros(n_frames, dtype=np.int64)
93
+ label[start-1:end] = 1 # 1-indexed -> 0-indexed slice
94
+ self.labels.append(label)
95
+ else:
96
+ self.labels = None # train, no label
97
+
98
+ # Collect the window indexes
99
+ self.windows = [] # list of (clip_idx, start_frame)
100
+ for clip_idx, frames in enumerate(self.clips):
101
+ n_frames = len(frames)
102
+ for start in range(0, n_frames - window_size + 1, stride):
103
+ self.windows.append((clip_idx, start))
104
+
105
+ def __len__(self) -> int:
106
+ return len(self.windows)
107
+
108
+ def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]:
109
+ """
110
+ Returns:
111
+ frames: (T, C, H, W) tensor
112
+ label: (T,) tensor of 0/1 (train: all zeros, test: from gt)
113
+ """
114
+ # Read frames and label
115
+ clip_idx, start_frame = self.windows[idx]
116
+
117
+ # Take the frames within frame range
118
+ window_frames = self.clips[clip_idx][start_frame : start_frame + self.window_size] # shape: (T, H, W) uint8
119
+
120
+ # Check labels based on split
121
+ if self.split == "test":
122
+ labels_np = self.labels[clip_idx][start_frame : start_frame + self.window_size]
123
+ labels = torch.from_numpy(labels_np) # int64 tensor
124
+ else:
125
+ labels = torch.zeros(self.window_size, dtype=torch.long)
126
+
127
+ # Convert window array to tensor and reshape it
128
+ window_tensor = torch.from_numpy(window_frames).float() / 255.0
129
+ window_tensor = window_tensor.unsqueeze(1) # (T, H, W) -> (T, 1, H, W)
130
+
131
+ # Check for transforms
132
+ if self.transform is not None:
133
+ window_tensor = self.transform(window_tensor)
134
+
135
+ if self.mode == "prediction":
136
+ input_frames = window_tensor[:-1] # (15, 1, H, W) — first 15 window
137
+ target_frame = window_tensor[-1] # (1, H, W) — last frame, target
138
+ return input_frames, target_frame
139
+ else:
140
+ return window_tensor, labels
141
+
142
+ if __name__ == "__main__":
143
+ # Run sanity check
144
+ train_clips = [0,1,2,3,4,5,6,7,8,9,10,11,12] # 13 clip
145
+ val_clips = [13,14,15] # 3 clip
146
+
147
+ # Train
148
+ ds_train = UCSDDataset(root="data/ucsd/raw", subset="ped2", clip_indices=train_clips, transform=transform, split="train")
149
+ print(f"Train: {len(ds_train.clips)} clips, {len(ds_train)} windows")
150
+ print(f"First clip shape: {ds_train.clips[0].shape}")
151
+
152
+ # Validation
153
+ ds_val = UCSDDataset(root="data/ucsd/raw", subset="ped2", clip_indices=val_clips, transform=transform, split="train")
154
+ print(f"Val: {len(ds_val.clips)} clips, {len(ds_val)} windows")
155
+ print(f"Val labels: {ds_val.labels}") # Should be None
156
+
157
+ # Test
158
+ ds_test = UCSDDataset(root="data/ucsd/raw", subset="ped2", split="test", transform=transform)
159
+ print(f"Test: {len(ds_test.clips)} clips, {len(ds_test)} windows")
160
+ print(f"First label sum: {ds_test.labels[0].sum()}/{len(ds_test.labels[0])}")
161
+
162
+ # Test getitem
163
+ sample, label = ds_train[0]
164
+ print(f"\nSample 0 (train):")
165
+ print(f" Sample shape: {sample.shape}, dtype: {sample.dtype}")
166
+ print(f" Sample range: [{sample.min():.3f}, {sample.max():.3f}]")
167
+ print(f" Label shape: {label.shape}, sum: {label.sum()}")
168
+
169
+ sample, label = ds_val[0]
170
+ print(f"\nSample 0 (test):")
171
+ print(f" Sample shape: {sample.shape}")
172
+ print(f" Label shape: {label.shape}, sum: {label.sum()}")
173
+
174
+ # Random middle sample
175
+ sample, label = ds_train[len(ds_train) // 2]
176
+ print(f"\nMiddle train sample shape: {sample.shape}")
177
+
178
+ # Transform check
179
+ print(sample.shape) # torch.Size([16, 1, 128, 128])
180
+
181
+ # Prediction
182
+ ds = UCSDDataset(root="data/ucsd/raw", subset="ped2", split="train",
183
+ clip_indices=list(range(13)), transform=transform, mode="prediction")
184
+ inp, tgt = ds[0]
185
+ print(f"input: {inp.shape}") # expected (15, 1, 128, 128)
186
+ print(f"target: {tgt.shape}") # expected (1, 128, 128)
src/data/video_transforms.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Video transforms for UCSD anomaly detection.
3
+ M1 minimal: Resize + Normalize. No random augmentation.
4
+ """
5
+
6
+ import torch
7
+ from torchvision.transforms import v2
8
+
9
+
10
+ # Input: float32 tensor, shape (T, 1, H, W), range [0, 1]
11
+ # Output: float32 tensor, shape (T, 1, 128, 128), range ~[-1, 1]
12
+ transform = v2.Compose([
13
+ v2.Resize(size=(128, 128), antialias=True),
14
+ v2.Normalize(mean=[0.5], std=[0.5]),
15
+ ])
16
+
17
+
18
+ if __name__ == "__main__":
19
+ # Dummy window
20
+ x = torch.rand(16, 1, 240, 360) # (T, C, H, W)
21
+ print(f"Before: shape={x.shape}, range=[{x.min():.3f}, {x.max():.3f}]")
22
+
23
+ y = transform(x)
24
+ print(f"After: shape={y.shape}, range=[{y.min():.3f}, {y.max():.3f}]")
25
+
26
+ # Mean/std after normalize, beklenen ~0 mean ~0.577 std (uniform [-1,1] için)
27
+ print(f"After mean={y.mean():.3f}, std={y.std():.3f}")
src/eval/__init__.py ADDED
File without changes
src/eval/metrics.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module for measuring metrics.
3
+ """
4
+
5
+ from sklearn.metrics import roc_auc_score, roc_curve
6
+ import numpy as np
7
+
8
+
9
+ def compute_auc(all_scores, all_labels):
10
+ """Frame-level ROC-AUC."""
11
+ return roc_auc_score(all_labels, all_scores)
12
+
13
+
14
+ def compute_eer(all_scores, all_labels):
15
+ """Equal Error Rate: The error at point FPR == FNR"""
16
+ fpr, tpr, _ = roc_curve(all_labels, all_scores)
17
+ fnr = 1 - tpr
18
+
19
+ # FPR and FNR's nearest index
20
+ idx = np.nanargmin(np.abs(fpr - fnr))
21
+ eer = (fpr[idx] + fnr[idx]) / 2
22
+ return eer
src/eval/visualization.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module for visualizations.
3
+ """
4
+
5
+ import matplotlib.pyplot as plt
6
+ import numpy as np
7
+
8
+
9
+ def plot_error_distribution(all_scores, all_labels, save_path="docs/error_dist.png"):
10
+ """
11
+ Function that plots the error distribution between normal and anomaly scores using label masking.
12
+ """
13
+
14
+ # Mask the labels
15
+ normal_scores = all_scores[all_labels == 0]
16
+ anomaly_scores = all_scores[all_labels == 1]
17
+
18
+ # Plot the graph
19
+ plt.figure(figsize=(10, 5))
20
+ plt.hist(normal_scores, bins=50, alpha=0.6, label="Normal", density=True)
21
+ plt.hist(anomaly_scores, bins=50, alpha=0.6, label="Anomaly", density=True)
22
+ plt.xlabel("Reconstruction error")
23
+ plt.ylabel("Density")
24
+ plt.title("Per-frame reconstruction error: normal vs anomaly")
25
+ plt.legend()
26
+ plt.savefig(save_path, dpi=120, bbox_inches="tight")
27
+ print(f"saved: {save_path}")
src/export/__init__.py ADDED
File without changes
src/export/onnx_export.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Export the M3 U-Net predictor to ONNX, with a PyTorch-vs-ONNX parity check.
3
+ """
4
+
5
+ import torch
6
+ import numpy as np
7
+ import onnxruntime as ort
8
+ from src.models.predictor import UNetPredictor
9
+
10
+
11
+ if __name__ == "__main__":
12
+ device = "cpu" # generally conducts on cpu
13
+
14
+ # Load trained model
15
+ model = UNetPredictor().to(device)
16
+ ckpt = torch.load("checkpoints/pred_best.pt", map_location=device)
17
+ model.load_state_dict(ckpt["model_state"])
18
+ model.eval()
19
+
20
+ # Example input — same shape as a real window
21
+ dummy = torch.randn(1, 15, 1, 128, 128) # (B, 15, 1, H, W)
22
+
23
+ # Export to ONNX
24
+ torch.onnx.export(
25
+ model, # model
26
+ dummy, # sample input (for trace)
27
+ "checkpoints/model.onnx", # output filename
28
+ input_names=["input"], # input node name
29
+ output_names=["output"], # output node name
30
+ dynamic_axes={ # dynamic batch axis
31
+ "input": {0: "batch"},
32
+ "output": {0: "batch"},
33
+ },
34
+ opset_version=18,
35
+ )
36
+
37
+ # Parity test — PyTorch vs ONNX Runtime
38
+ with torch.no_grad():
39
+ torch_out = model(dummy).cpu().numpy()
40
+
41
+ sess = ort.InferenceSession("checkpoints/model.onnx")
42
+ input_name = sess.get_inputs()[0].name
43
+ onnx_out = sess.run(None, {input_name: dummy.numpy()})[0]
44
+
45
+ # Compare model outputs
46
+ max_diff = np.abs(torch_out - onnx_out).max()
47
+ print(max_diff) # expected ~1e-5
48
+ assert max_diff < 1e-4
src/inference/__init__.py ADDED
File without changes
src/inference/predictor.py ADDED
File without changes
src/inference/scoring.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module for per-frame anomaly scoring on UCSD test split.
3
+
4
+ Pipeline: model reconstruction -> per-frame error -> overlapping-window
5
+ averaging -> per-clip frame-aligned anomaly scores.
6
+ """
7
+
8
+ import numpy as np
9
+ import torch
10
+ import torch.nn as nn
11
+ from torch.utils.data import DataLoader
12
+ from scipy.ndimage import gaussian_filter1d
13
+ from src.data.ucsd_loader import UCSDDataset
14
+ from src.models.autoencoder import AutoEncoder
15
+ from src.data.video_transforms import transform
16
+
17
+
18
+ def smooth_scores(scores: np.ndarray, sigma: float = 2.0) -> np.ndarray:
19
+ """Temporal Gaussian smoothing on a single clip's per-frame scores."""
20
+ return gaussian_filter1d(scores, sigma=sigma)
21
+
22
+
23
+ def compute_frame_errors(model: nn.Module, dataset: UCSDDataset, device: str) -> dict:
24
+ """
25
+ Compute per-frame reconstruction error for every clip in the test set,
26
+ averaging across overlapping windows.
27
+
28
+ Returns:
29
+ dict mapping clip_idx -> (scores, labels)
30
+ - scores: np.ndarray shape (n_frames,), avg reconstruction error per frame
31
+ - labels: np.ndarray shape (n_frames,), 0/1 ground truth per frame
32
+ """
33
+ model.eval()
34
+
35
+ # Prepare an accumulator for every clip
36
+ error_sum = {}
37
+ count = {}
38
+ for clip_idx in range(len(dataset.clips)):
39
+ n_frames = len(dataset.clips[clip_idx])
40
+ error_sum[clip_idx] = np.zeros(n_frames, dtype=np.float64)
41
+ count[clip_idx] = np.zeros(n_frames, dtype=np.float64)
42
+
43
+ # Give out the windows towards the model
44
+ loader = DataLoader(dataset, batch_size=1, shuffle=False)
45
+
46
+ with torch.no_grad():
47
+ for idx, (window, _labels) in enumerate(loader):
48
+ # Window shape: (1, T, C, H, W) -- batch size 1
49
+ window = window.to(device)
50
+
51
+ # Reconstruction
52
+ out = model(window)
53
+ recon = out[0] if isinstance(out, tuple) else out
54
+
55
+ # Calculate per-frame error with taking the mean based on (C,H,W) channels
56
+ per_frame_err = torch.mean(((window - recon)**2), dim=(0, 2, 3, 4)).cpu().numpy() # shape: (T,)
57
+
58
+ # Take a particular window from a particular clip
59
+ clip_idx, start_frame = dataset.windows[idx]
60
+
61
+ # For every t, global frame = start_frame + t
62
+ error_sum[clip_idx][start_frame : start_frame + dataset.window_size] += per_frame_err
63
+ count[clip_idx][start_frame : start_frame + dataset.window_size] += 1
64
+
65
+ # Ortalama al + ground truth'u hizala
66
+ results = {}
67
+ for clip_idx in error_sum:
68
+ # Counts and errors
69
+ counts = count[clip_idx]
70
+ errs = error_sum[clip_idx]
71
+
72
+ # Log the number of frames that aren't valid
73
+ print(f"clip {clip_idx}: {(counts==0).sum()} frames with no window coverage")
74
+
75
+ # Valid frame filter
76
+ valid = counts > 0
77
+
78
+ # Take out the average which gives the result of average error
79
+ scores = errs[valid] / counts[valid] # Only valid frames
80
+ scores = smooth_scores(scores, sigma=1.0) # Clip based smoothing
81
+ labels = dataset.labels[clip_idx][valid] # Apply same mask
82
+
83
+ results[clip_idx] = (scores, labels)
84
+
85
+ return results
86
+
87
+
88
+ def aggregate_all(results: dict) -> tuple:
89
+ """
90
+ Flatten per-clip results into two 1D arrays for global AUC.
91
+
92
+ Returns:
93
+ all_scores: np.ndarray (total_frames,)
94
+ all_labels: np.ndarray (total_frames,)
95
+ """
96
+ scores_list = []
97
+ labels_list = []
98
+
99
+ # Append corresponding clip's (scores, labels) by order
100
+ for clip_idx in results:
101
+ scores, labels = results[clip_idx]
102
+ scores_list.append(scores)
103
+ labels_list.append(labels)
104
+
105
+ # Concatenate the results on 1D numpy array
106
+ all_scores = np.concatenate(scores_list)
107
+ all_labels = np.concatenate(labels_list)
108
+
109
+ return all_scores, all_labels
110
+
111
+
112
+ def compute_prediction_errors(model: nn.Module, dataset: UCSDDataset, device: str) -> dict:
113
+ """
114
+ Per-frame prediction error for M3.
115
+ Each window (15 input -> 1 target) scores ONE frame: the target frame
116
+ at index (start_frame + 15) in its clip.
117
+ """
118
+ model.eval()
119
+
120
+ # Per-clip accumulator. Many frames are never a target:
121
+ # the first 15 frames of each clip are always inputs, never predicted.
122
+ error_sum = {}
123
+ count = {}
124
+ for clip_idx in range(len(dataset.clips)):
125
+ n_frames = len(dataset.clips[clip_idx])
126
+ error_sum[clip_idx] = np.zeros(n_frames, dtype=np.float64)
127
+ count[clip_idx] = np.zeros(n_frames, dtype=np.float64)
128
+
129
+ loader = DataLoader(dataset, batch_size=1, shuffle=False)
130
+
131
+ with torch.no_grad():
132
+ for idx, (inputs, target) in enumerate(loader):
133
+ # inputs: (1,15,1,H,W), target: (1,1,H,W)
134
+ inputs, target = inputs.to(device), target.to(device)
135
+ pred = model(inputs) # (1,1,H,W)
136
+
137
+ # Single target frame -> one scalar error (mean over C,H,W)
138
+ err = ((pred - target) ** 2).mean().item()
139
+
140
+ # Which clip / which target frame does this window predict?
141
+ clip_idx, start_frame = dataset.windows[idx]
142
+ target_idx = start_frame + 15 # first 15 are inputs, 16th is target
143
+ error_sum[clip_idx][target_idx] += err
144
+ count[clip_idx][target_idx] += 1
145
+
146
+ # Average + align ground truth (count>0 mask, like M1/M2).
147
+ # NOTE: first 15 frames + uncovered frames have count==0, masked out.
148
+ results = {}
149
+ for clip_idx in error_sum:
150
+ counts = count[clip_idx]
151
+ errs = error_sum[clip_idx]
152
+
153
+ # Log frames with no coverage (expected: at least the first 15)
154
+ print(f"clip {clip_idx}: {(counts==0).sum()} frames with no prediction coverage")
155
+
156
+ # Keep only frames that were predicted at least once
157
+ valid = counts > 0
158
+
159
+ scores = errs[valid] / counts[valid] # average error per frame
160
+ scores = smooth_scores(scores, sigma=1.0) # clip-level temporal smoothing
161
+ labels = dataset.labels[clip_idx][valid] # same mask -> alignment
162
+
163
+ results[clip_idx] = (scores, labels)
164
+
165
+ return results
166
+
167
+
168
+ if __name__ == "__main__":
169
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
170
+
171
+ # Modeli yükle (eğittiğin best checkpoint)
172
+ model = AutoEncoder().to(device)
173
+ ckpt = torch.load("checkpoints/ae_best.pt", map_location=device)
174
+ model.load_state_dict(ckpt["model_state"])
175
+
176
+ # Test dataset — clip_indices YOK (tüm 12 clip), split="test"
177
+ test_ds = UCSDDataset(root="data/ucsd/raw", subset="ped2", split="test", transform=transform)
178
+
179
+ results = compute_frame_errors(model, test_ds, device)
180
+ all_scores, all_labels = aggregate_all(results)
181
+
182
+ # Sanity check
183
+ print(f"shape: {all_scores.shape}, {all_labels.shape}") # same, 1D
184
+ print(f"anomaly frames: {all_labels.sum()}/{len(all_labels)}")
185
+
186
+ normal_mean = all_scores[all_labels == 0].mean()
187
+ anomaly_mean = all_scores[all_labels == 1].mean()
188
+ print(f"normal mean error: {normal_mean:.6f}")
189
+ print(f"anomaly mean error: {anomaly_mean:.6f}")
src/inference/stream.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Streaming anomaly detection for the M3 predictor.
3
+ Rolling 15-frame buffer -> predict next frame -> per-frame anomaly score (+ heatmap).
4
+ """
5
+
6
+ import os
7
+ import glob
8
+ import numpy as np
9
+ import cv2
10
+ import onnxruntime as ort
11
+ from collections import deque
12
+ from src.data.video_transforms import transform # same transform as training
13
+ import torch
14
+
15
+
16
+ class AnomalyStream:
17
+ def __init__(self, onnx_path: str, buffer_size: int = 15):
18
+ self.sess = ort.InferenceSession(onnx_path)
19
+ self.input_name = self.sess.get_inputs()[0].name
20
+ self.buffer_size = buffer_size
21
+ self.buffer = deque(maxlen=buffer_size) # last 15 preprocessed frame
22
+
23
+ def preprocess(self, frame_bgr: np.ndarray) -> torch.Tensor:
24
+ """
25
+ Raw video frame (H,W,3 BGR) -> training format (1, H, W) [-1,1] grayscale 128x128.
26
+ """
27
+ # BGR -> grayscale (numpy, uint8, (H,W))
28
+ gray = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2GRAY)
29
+
30
+ # numpy -> torch, float, [0,1]
31
+ gray_t = torch.from_numpy(gray).float() / 255.0 # (H, W)
32
+
33
+ # transform expects (T, C, H, W) — make it (1, 1, H, W): T=1, C=1
34
+ gray_t = gray_t.unsqueeze(0).unsqueeze(0) # (1, 1, H, W)
35
+
36
+ # apply training transform (resize 128, normalize [-1,1])
37
+ gray_t = transform(gray_t) # (1, 1, 128, 128)
38
+
39
+ # drop the T axis -> (1, 128, 128) = (C, H, W) for buffering
40
+ return gray_t.squeeze(0) # (1, 128, 128)
41
+
42
+ def push(self, frame_bgr: np.ndarray):
43
+ """
44
+ Add one frame. Returns (score, heatmap) or None if still warming up.
45
+ """
46
+ frame_t = self.preprocess(frame_bgr) # (1,128,128)
47
+
48
+ # Cold start: wait for buffer to warm up (first 15 frames)
49
+ if len(self.buffer) < self.buffer_size:
50
+ self.buffer.append(frame_t)
51
+ return None # warming up
52
+
53
+ # buffer: 15 frame, every frame shaped (1,128,128) = (C,H,W)
54
+ # stack -> (15, 1, 128, 128), then batch axis -> (1, 15, 1, 128, 128)
55
+ stacked = torch.stack(list(self.buffer)) # (15, 1, 128, 128)
56
+ inp = stacked.unsqueeze(0).numpy() # (1, 15, 1, 128, 128) numpy
57
+ pred = self.sess.run(None, {self.input_name: inp})[0] # (1,1,128,128)
58
+
59
+ # Real frame (target) = this new frame
60
+ actual = frame_t.numpy()[None, ...] # (1,1,128,128) -- shape matching
61
+
62
+ # Per-pixel error -> heatmap, mean -> score
63
+ error_map = (pred - actual) ** 2 # (1, 1, 128, 128)
64
+ heatmap = error_map[0, 0] # (128, 128) — spatial harita, frontend için
65
+ score = float(error_map.mean()) # scaler anomaly score
66
+
67
+ # Update the buffer
68
+ self.buffer.append(frame_t)
69
+
70
+ return score, heatmap, frame_t.numpy()[0] # (128,128) preprocessed target image
71
+
72
+
73
+ def process_video(video_path, onnx_path, top_n=5):
74
+ """Run the stream over a video file, collect per-frame scores."""
75
+ stream = AnomalyStream(onnx_path)
76
+ cap = cv2.VideoCapture(video_path)
77
+
78
+ scores = []
79
+ scored_records = [] # (score, frame_idx, heatmap, frame_image)
80
+
81
+ # Measure the FPS
82
+ fps = cap.get(cv2.CAP_PROP_FPS)
83
+
84
+ frame_idx = 0
85
+ while True:
86
+ ret, frame = cap.read()
87
+ if not ret:
88
+ break
89
+ result = stream.push(frame)
90
+ if result is None:
91
+ scores.append(None)
92
+ else:
93
+ score, heatmap, frame_img = result
94
+ scores.append(score)
95
+ scored_records.append((score, frame_idx, heatmap, frame_img))
96
+ frame_idx += 1
97
+ cap.release()
98
+
99
+ # Top-N highest scored frame
100
+ top = sorted(scored_records, key=lambda r: r[0], reverse=True)[:top_n]
101
+ top_anomalies = [
102
+ {"frame_idx": idx, "score": float(s), "heatmap": hmap, "frame": img}
103
+ for (s, idx, hmap, img) in top
104
+ ]
105
+
106
+ return scores, top_anomalies, fps
107
+
108
+
109
+ def process_frames(frame_dir: str, onnx_path: str):
110
+ """
111
+ Run the stream over a directory of .tif frames (UCSD format).
112
+ Mirrors process_video but reads ordered image files instead of decoding video.
113
+ Used to verify the streaming pipeline matches eval scoring.
114
+ """
115
+ stream = AnomalyStream(onnx_path)
116
+
117
+ # UCSD frames: sorted .tif files in the clip dir
118
+ frame_paths = sorted(glob.glob(os.path.join(frame_dir, "*.tif")))
119
+
120
+ scores = []
121
+ for path in frame_paths:
122
+ # cv2.imread reads as BGR (H,W,3) even for grayscale .tif -> preprocess handles BGR->gray
123
+ frame = cv2.imread(path)
124
+ result = stream.push(frame)
125
+ if result is None:
126
+ scores.append(None) # warming up (first 15)
127
+ else:
128
+ score, heatmap = result
129
+ scores.append(score)
130
+
131
+ return scores
132
+
133
+
134
+ if __name__ == "__main__":
135
+ # Smoke test for streaming
136
+ onnx_path = "checkpoints/model.onnx"
137
+ clip_dir = "data/ucsd/raw/UCSDped2/Test/Test001" # a test clip
138
+
139
+ scores, top = process_video("/tmp/test001.mp4", "checkpoints/model.onnx", top_n=5)
140
+ print(f"scored: {len([s for s in scores if s is not None])}, top anomalies: {len(top)}")
141
+ for t in top:
142
+ print(f" frame {t['frame_idx']}: score {t['score']:.6e}, heatmap {t['heatmap'].shape}, frame {t['frame'].shape}")
src/models/__init__.py ADDED
File without changes
src/models/autoencoder.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module for autoencoder model.
3
+ """
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+
8
+
9
+ class AutoEncoder(nn.Module):
10
+ """
11
+ Auto encoder model class.
12
+ """
13
+
14
+ def __init__(self):
15
+ super().__init__()
16
+ self.network = nn.Sequential(
17
+ # Encoder layers
18
+ nn.Conv3d(in_channels=1, out_channels=16, kernel_size=(3, 3, 3), stride=(1, 2, 2), padding=1),
19
+ nn.GroupNorm(num_groups=8, num_channels=16),
20
+ nn.LeakyReLU(),
21
+
22
+ nn.Conv3d(in_channels=16, out_channels=32, kernel_size=(3, 3, 3), stride=(2, 2, 2), padding=1),
23
+ nn.GroupNorm(num_groups=8, num_channels=32),
24
+ nn.LeakyReLU(),
25
+
26
+ nn.Conv3d(in_channels=32, out_channels=64, kernel_size=(3, 3, 3), stride=(2, 2, 2), padding=1),
27
+ nn.GroupNorm(num_groups=8, num_channels=64),
28
+ nn.LeakyReLU(),
29
+
30
+ # Bottleneck
31
+ nn.Conv3d(in_channels=64, out_channels=16, kernel_size=(3, 3, 3), stride=(1, 1, 1), padding=1),
32
+ nn.GroupNorm(num_groups=8, num_channels=16),
33
+ nn.LeakyReLU(),
34
+
35
+ # Decoder layers
36
+ nn.ConvTranspose3d(in_channels=16, out_channels=32, kernel_size=(3, 3, 3), stride=(2, 2, 2), padding=1, output_padding=(1, 1, 1)),
37
+ nn.GroupNorm(num_groups=8, num_channels=32),
38
+ nn.LeakyReLU(),
39
+
40
+ nn.ConvTranspose3d(in_channels=32, out_channels=16, kernel_size=(3, 3, 3), stride=(2, 2, 2), padding=1, output_padding=(1, 1, 1)),
41
+ nn.GroupNorm(num_groups=8, num_channels=16),
42
+ nn.LeakyReLU(),
43
+
44
+ # Output layer
45
+ nn.ConvTranspose3d(in_channels=16, out_channels=1, kernel_size=(3, 3, 3), stride=(1, 2, 2), padding=1, output_padding=(0, 1, 1)),
46
+ nn.Tanh(),
47
+ )
48
+
49
+ def forward(self, x):
50
+ # Permute to match the shape that dataloader gives
51
+ x = x.permute(0, 2, 1, 3, 4) # (B,T,C,H,W) -> (B,C,T,H,W)
52
+ x = self.network(x)
53
+ x = x.permute(0, 2, 1, 3, 4) # backwards: (B,C,T,H,W) -> (B,T,C,H,W)
54
+ return x
55
+
56
+
57
+ if __name__ == "__main__":
58
+ # Smoke test to assert that shapes are correctly matches
59
+ model = AutoEncoder()
60
+ x = torch.randn(2, 16, 1, 128, 128)
61
+
62
+ # debug: seperate variable to permute manually
63
+ xd = x.permute(0,2,1,3,4)
64
+ for layer in model.network:
65
+ xd = layer(xd)
66
+ if isinstance(xd, torch.Tensor):
67
+ print(type(layer).__name__, tuple(xd.shape))
68
+
69
+ # real forward prop
70
+ out = model(x)
71
+ print("out:", tuple(out.shape))
72
+ assert out.shape == x.shape
src/models/memory_ae.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Memory-Augmented Autoencoder (MemAE) for video anomaly detection.
3
+
4
+ Encoder/decoder backbone = M1 AutoEncoder, unchanged. A memory module is
5
+ inserted between them: the decoder can only reconstruct from stored normal
6
+ prototypes, so anomalies (absent from memory) reconstruct poorly.
7
+
8
+ Ref: Gong et al. 2019, "Memorizing Normality to Detect Anomaly" (ICCV).
9
+ """
10
+
11
+ import torch
12
+ import torch.nn as nn
13
+ import torch.nn.functional as F
14
+
15
+
16
+ class MemoryModule(nn.Module):
17
+ """
18
+ Memory bank with sparse attention-based addressing.
19
+
20
+ Forward: bottleneck feature -> queries -> address memory -> reconstructed
21
+ feature (+ attention weights for entropy loss / sparsity inspection).
22
+
23
+ Args:
24
+ n_slots: N, number of memory items (paper value)
25
+ feat_dim: C, dimension of each memory item = bottleneck channel dim (16)
26
+ shrink_thres: lambda, sparse addressing threshold (paper value)
27
+ """
28
+
29
+ def __init__(self, n_slots: int, feat_dim: int = 16, shrink_thres: float = None):
30
+ super().__init__()
31
+ self.n_slots = n_slots
32
+ self.feat_dim = feat_dim
33
+
34
+ # Edge case check
35
+ if shrink_thres is None:
36
+ shrink_thres = 1.0 / n_slots # lambda, dependant on N
37
+ self.shrink_thres = shrink_thres
38
+
39
+ # Memory bank: learnable (N, C) matrix, trained end-to-end via backprop
40
+ self.memory = nn.Parameter(torch.randn(size=(self.n_slots, self.feat_dim)))
41
+
42
+ def forward(self, z: torch.Tensor):
43
+ """
44
+ Args:
45
+ z: bottleneck feature, shape (B, C, T, H, W) = (B, 16, 4, 16, 16)
46
+ Returns:
47
+ z_hat: reconstructed feature, same shape as z
48
+ attn: attention weights, shape (B, n_queries, N) -- for loss/viz
49
+ """
50
+ B, C, T, H, W = z.shape
51
+ n_queries = T * H * W # 4*16*16 = 1024
52
+
53
+ # (B,C,T,H,W) -> queries (B, n_queries, C)
54
+ z = z.permute(dims=(0, 2, 3, 4, 1))
55
+ query = z.reshape(shape=(B, n_queries, C)) # shape (B, n_queries, C)
56
+
57
+ # Cosine similarity: query vs her memory slot.
58
+ query_n = F.normalize(query, dim=-1) # (B, n_queries, C)
59
+ memory_n = F.normalize(self.memory, dim=-1) # (N, C)
60
+ sim = query_n @ memory_n.t() # (B, n_queries, N)
61
+
62
+ # Softmax over N
63
+ attn = F.softmax(sim, dim=-1) # -1 dim to autocalculate dimensions, shape (B, n_queries, N)
64
+
65
+ # Sparse addressing: hard shrinkage + renormalize
66
+ eps = 1e-12
67
+ # hard shrinkage
68
+ attn = F.relu(attn - self.shrink_thres) * attn / (torch.abs(attn - self.shrink_thres) + eps)
69
+ # renormalize
70
+ attn = attn / (attn.sum(dim=-1, keepdim=True) + eps)
71
+
72
+ # Weighted sum: with plain (unnormalized) memory
73
+ z_hat_flat = attn @ self.memory # (B,n_queries,N) @ (N,C) = (B,n_queries,C)
74
+
75
+ # queries -> (B,C,T,H,W) backwards
76
+ # Backwards of the first step: reshape -> (B,T,H,W,C), after permute -> (B,C,T,H,W)
77
+ z_hat = z_hat_flat.reshape(B, T, H, W, C).permute(0, 4, 1, 2, 3) # (B,C,T,H,W)
78
+
79
+ return z_hat, attn
80
+
81
+
82
+ class MemoryAE(nn.Module):
83
+ """
84
+ M1 encoder + MemoryModule + M1 decoder.
85
+ Encoder/decoder backbone unchanged from M1 (clean ablation).
86
+ """
87
+
88
+ def __init__(self, n_slots: int, shrink_thres: float = None):
89
+ super().__init__()
90
+
91
+ # Edge case check
92
+ if shrink_thres is None:
93
+ shrink_thres = 1.0 / n_slots # lambda, dependant on N
94
+
95
+ # Encoder layers
96
+ self.encoder = nn.Sequential(
97
+ # enc1
98
+ nn.Conv3d(1, 16, (3,3,3), stride=(1,2,2), padding=1),
99
+ nn.GroupNorm(8, 16),
100
+ nn.LeakyReLU(),
101
+ # enc2
102
+ nn.Conv3d(16, 32, (3,3,3), stride=(2,2,2), padding=1),
103
+ nn.GroupNorm(8, 32),
104
+ nn.LeakyReLU(),
105
+ # enc3
106
+ nn.Conv3d(32, 64, (3,3,3), stride=(2,2,2), padding=1),
107
+ nn.GroupNorm(8, 64),
108
+ nn.LeakyReLU(),
109
+ # bottleneck — encoder's last piece
110
+ nn.Conv3d(64, 16, (3,3,3), stride=(1,1,1), padding=1),
111
+ nn.GroupNorm(8, 16),
112
+ nn.LeakyReLU(),
113
+ )
114
+ # Memory layer
115
+ self.memory = MemoryModule(n_slots=n_slots, feat_dim=16, shrink_thres=shrink_thres)
116
+ # Decoder layers
117
+ self.decoder = nn.Sequential(
118
+ # dec1
119
+ nn.ConvTranspose3d(16, 32, (3,3,3), stride=(2,2,2), padding=1, output_padding=(1,1,1)),
120
+ nn.GroupNorm(8, 32),
121
+ nn.LeakyReLU(),
122
+ # dec2
123
+ nn.ConvTranspose3d(32, 16, (3,3,3), stride=(2,2,2), padding=1, output_padding=(1,1,1)),
124
+ nn.GroupNorm(8, 16),
125
+ nn.LeakyReLU(),
126
+ # dec3
127
+ nn.ConvTranspose3d(16, 1, (3,3,3), stride=(1,2,2), padding=1, output_padding=(0,1,1)),
128
+ nn.Tanh(),
129
+ )
130
+
131
+ def forward(self, x: torch.Tensor):
132
+ """
133
+ Args:
134
+ x: (B, T, C, H, W) -- loader format (same with M1)
135
+ Returns:
136
+ recon: (B, T, C, H, W)
137
+ attn: (B, n_queries, N)
138
+ """
139
+ # M1 permute logic: loader (B,T,C,H,W) -> conv (B,C,T,H,W)
140
+ x = x.permute(0, 2, 1, 3, 4) # (B,C,T,H,W)
141
+
142
+ # encoder -> bottleneck
143
+ z = self.encoder(x) # (B, 16, 4, 16, 16)
144
+
145
+ # memory addressing
146
+ z_hat, attn = self.memory(z) # (B, 16, 4, 16, 16), (B, 1024, N)
147
+
148
+ # decoder
149
+ recon = self.decoder(z_hat) # (B, C, T, H, W)
150
+
151
+ # permute backwards to loader format
152
+ recon = recon.permute(0, 2, 1, 3, 4) # (B,T,C,H,W)
153
+
154
+ return recon, attn
155
+
156
+
157
+ if __name__ == "__main__":
158
+ # Smoke test
159
+ model = MemoryAE(n_slots=2000) # paper N
160
+ x = torch.randn(2, 16, 1, 128, 128)
161
+
162
+ # Control piece by piece
163
+ xp = x.permute(0,2,1,3,4) # (2,16,1,128,128) -> (2,1,16,128,128)
164
+ z = model.encoder(xp)
165
+ print("bottleneck:", z.shape) # should be (2, 16, 4, 16, 16)
166
+
167
+ z_hat, attn = model.memory(z)
168
+ print("z_hat:", z_hat.shape, "attn:", attn.shape) # (2,16,4,16,16), (2,1024,2000)
169
+
170
+ recon, attn = model(x)
171
+ active_frac = (attn > 0).float().mean()
172
+ active_per_query = (attn > 0).float().sum(dim=-1).mean()
173
+ print(f"shrink_thres (lambda): {model.memory.shrink_thres}")
174
+ print(f"active slot fraction: {active_frac:.4f}")
175
+ print(f"avg active slots/query: {active_per_query:.1f} / {model.memory.n_slots}")
src/models/predictor.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module for UNet based predictor.
3
+ """
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+
9
+
10
+ class UNetPredictor(nn.Module):
11
+ """
12
+ U net based predictor model class.
13
+ """
14
+
15
+ def __init__(self):
16
+ super().__init__()
17
+
18
+ # Encoder blocks
19
+ self.enc1 = nn.Sequential(
20
+ nn.Conv2d(in_channels=15, out_channels=32, kernel_size=(3, 3), padding=1),
21
+ nn.GroupNorm(num_groups=8, num_channels=32),
22
+ nn.LeakyReLU()
23
+ )
24
+ self.enc2 = nn.Sequential(
25
+ nn.Conv2d(in_channels=32, out_channels=64, kernel_size=(3, 3), padding=1),
26
+ nn.GroupNorm(num_groups=8, num_channels=64),
27
+ nn.LeakyReLU()
28
+ )
29
+ self.enc3 = nn.Sequential(
30
+ nn.Conv2d(in_channels=64, out_channels=128, kernel_size=(3, 3), padding=1),
31
+ nn.GroupNorm(num_groups=8, num_channels=128),
32
+ nn.LeakyReLU()
33
+ )
34
+ self.bottleneck = nn.Sequential(
35
+ nn.Conv2d(in_channels=128, out_channels=256, kernel_size=(3, 3), padding=1),
36
+ nn.GroupNorm(num_groups=8, num_channels=256),
37
+ nn.LeakyReLU()
38
+ )
39
+
40
+ # Decoder blocks
41
+ self.up3 = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=False)
42
+ self.dec3 = nn.Sequential(
43
+ nn.Conv2d(in_channels=384, out_channels=128, kernel_size=(3, 3), padding=1),
44
+ nn.GroupNorm(num_groups=8, num_channels=128),
45
+ nn.LeakyReLU()
46
+ )
47
+ self.up2 = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=False)
48
+ self.dec2 = nn.Sequential(
49
+ nn.Conv2d(in_channels=192, out_channels=64, kernel_size=(3, 3), padding=1),
50
+ nn.GroupNorm(num_groups=8, num_channels=64),
51
+ nn.LeakyReLU()
52
+ )
53
+ self.up1 = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=False)
54
+ self.dec1 = nn.Sequential(
55
+ nn.Conv2d(in_channels=96, out_channels=32, kernel_size=(3, 3), padding=1),
56
+ nn.GroupNorm(num_groups=8, num_channels=32),
57
+ nn.LeakyReLU()
58
+ )
59
+
60
+ # Output layer
61
+ self.out = nn.Sequential(
62
+ nn.Conv2d(in_channels=32, out_channels=1, kernel_size=(3, 3), padding=1),
63
+ nn.Tanh()
64
+ )
65
+
66
+ # Pooling layer
67
+ self.pool = nn.MaxPool2d(kernel_size=(2, 2), stride=2)
68
+
69
+ def forward(self, x: torch.Tensor):
70
+ # x: (B, 15, 1, H, W) -> squeeze/reshape -> (B, 15, H, W)
71
+ x = x.squeeze(2) # # (B,15,1,H,W) -> (B,15,H,W)
72
+ s1 = self.enc1(x) # (B,32,128,128) <- skip1
73
+ s2 = self.enc2(self.pool(s1)) # (B,64,64,64) <- skip2
74
+ s3 = self.enc3(self.pool(s2)) # (B,128,32,32) <- skip3
75
+ b = self.bottleneck(self.pool(s3)) # (B,256,16,16)
76
+
77
+ d3 = self.dec3(torch.cat([self.up3(b), s3], dim=1)) # cat→384 -> 128, (B,128,32,32)
78
+ d2 = self.dec2(torch.cat([self.up2(d3), s2], dim=1)) # cat→192 -> 64, (B,64,64,64)
79
+ d1 = self.dec1(torch.cat([self.up1(d2), s1], dim=1)) # cat→96 -> 32, (B,32,128,128)
80
+
81
+ return self.out(d1) # (B,1,128,128)
82
+
83
+
84
+ if __name__ == "__main__":
85
+ model = UNetPredictor()
86
+ x = torch.randn(2, 15, 1, 128, 128)
87
+ out = model(x)
88
+ print(out.shape) # expected: (2, 1, 128, 128)
src/models/video_transformer.py ADDED
File without changes
src/training/__init__.py ADDED
File without changes
src/training/losses.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module for custom loss functions.
3
+ """
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+
9
+
10
+ class MemAELoss(nn.Module):
11
+ """MSE reconstruction + entropy regularization on attention weights."""
12
+
13
+ def __init__(self, entropy_weight: float = 0.0002):
14
+ super().__init__()
15
+ self.entropy_weight = entropy_weight # alpha
16
+ self.mse = nn.MSELoss()
17
+
18
+ def forward(self, recon, target, attn):
19
+ """
20
+ Args:
21
+ recon: (B, T, C, H, W) reconstruction
22
+ target: (B, T, C, H, W) input
23
+ attn: (B, n_queries, N) attention weights
24
+ Returns:
25
+ total_loss, (recon_loss, entropy_loss) # ayrı logla
26
+ """
27
+ eps = 1e-12
28
+
29
+ # Reconstruction
30
+ recon_loss = self.mse(recon, target)
31
+
32
+ # Entropy: per-query entropy, then mean
33
+ # E = mean( -sum_i ( w_i * log(w_i + eps) ) )
34
+ entropy = (-(attn * torch.log(attn + eps)).sum(dim=-1)).mean()
35
+
36
+ # Sum
37
+ total = recon_loss + self.entropy_weight * entropy
38
+
39
+ return total, (recon_loss, entropy)
40
+
41
+
42
+ class PredictionLoss(nn.Module):
43
+ """Intensity (L2) + gradient loss for future frame prediction."""
44
+
45
+ def __init__(self, grad_weight: float = 1.0):
46
+ super().__init__()
47
+ self.grad_weight = grad_weight # lambda_grad
48
+
49
+ def forward(self, pred, target):
50
+ """
51
+ Args:
52
+ pred: (B, 1, H, W) predicted frame
53
+ target: (B, 1, H, W) ground truth frame
54
+ Returns:
55
+ total, (intensity, gradient)
56
+ """
57
+ # Intensity (L2)
58
+ intensity = F.mse_loss(pred, target)
59
+
60
+ # Gradient loss
61
+ ## Horizontal (x) gradient (last axis = W)
62
+ pred_dx = torch.abs(pred[:, :, :, 1:] - pred[:, :, :, :-1])
63
+ target_dx = torch.abs(target[:, :, :, 1:] - target[:, :, :, :-1])
64
+
65
+ ## Vertical (y) gradient (the axis before last = H)
66
+ pred_dy = torch.abs(pred[:, :, 1:, :] - pred[:, :, :-1, :])
67
+ target_dy = torch.abs(target[:, :, 1:, :] - target[:, :, :-1, :])
68
+
69
+ ## loss: gradient differences
70
+ gradient = F.l1_loss(pred_dx, target_dx) + F.l1_loss(pred_dy, target_dy)
71
+
72
+ # Total
73
+ total = intensity + self.grad_weight * gradient
74
+
75
+ return total, (intensity, gradient)
76
+
77
+
78
+ if __name__ == "__main__":
79
+ # Smoke test
80
+ # loss_fn = MemAELoss()
81
+ # recon = torch.randn(2, 16, 1, 128, 128)
82
+ # target = torch.randn(2, 16, 1, 128, 128)
83
+ # attn = torch.softmax(torch.randn(2, 1024, 2000), dim=-1) # valid distribution
84
+ # total, (rl, ent) = loss_fn(recon, target, attn)
85
+ # print(f"total: {total.item():.4f}, recon: {rl.item():.4f}, entropy: {ent.item():.4f}")
86
+
87
+ # Prediction loss smoke test
88
+ loss_pred = PredictionLoss()
89
+ pred = torch.randn(2, 1, 128, 128)
90
+ target = torch.randn(2, 1, 128, 128)
91
+ total, (inten, grad) = loss_pred(pred, target)
92
+ print(f"total: {total.item():.4f}, intensity: {inten.item():.4f}, gradient: {grad.item():.4f}")
src/training/trainer.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Module for pytorch training and validation functions.
3
+ """
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.optim as optim
8
+ from torch.utils.data import DataLoader
9
+ from src.training.losses import MemAELoss, PredictionLoss
10
+
11
+
12
+ def train_one_epoch(model: nn.Module, dataloader: DataLoader, criterion: nn.MSELoss, optimizer: optim.Optimizer, device: str) -> float:
13
+ """
14
+ Function to train the vanilla autoencoder model on a single epoch. Returns the average training loss.
15
+ """
16
+
17
+ # Set the model on train mode
18
+ model.train()
19
+ running_loss = 0.0
20
+
21
+ # Iterate within dataloader
22
+ for tensors, labels in dataloader:
23
+ # Move the tensors and labels to device
24
+ tensors, labels = tensors.to(device), labels.to(device)
25
+
26
+ # Core 5-step optimization
27
+ optimizer.zero_grad(set_to_none=True) # set_to_none=True to optimize memory allocation
28
+ outputs = model(tensors)
29
+ loss = criterion(outputs, tensors)
30
+ loss.backward()
31
+ optimizer.step()
32
+
33
+ running_loss += loss.item() * tensors.size(0)
34
+
35
+ return running_loss / len(dataloader.dataset)
36
+
37
+
38
+ def validate(model: nn.Module, dataloader: DataLoader, criterion: nn.MSELoss, device: str) -> float:
39
+ """
40
+ Function to evaluate the vanilla autoencoder model. Returns the average validation loss.
41
+ """
42
+
43
+ # Set the model on eval mode
44
+ model.eval()
45
+ running_loss = 0.0
46
+
47
+ # Deactivate the gradients for memory optimization
48
+ # NOTE: inference_mode eliminates gradient overhead, making it faster than no_grad but we'll need the tensors for the future so, no_grad is safer to use
49
+ with torch.no_grad():
50
+ # Iterate within dataloader
51
+ for tensors, labels in dataloader:
52
+ # Move the tensors and labels to device
53
+ tensors, labels = tensors.to(device), labels.to(device)
54
+
55
+ outputs = model(tensors)
56
+ loss = criterion(outputs, tensors)
57
+
58
+ running_loss += loss.item() * tensors.size(0)
59
+
60
+ return running_loss / len(dataloader.dataset)
61
+
62
+
63
+ def train_one_epoch_memae(model: nn.Module, dataloader: DataLoader, criterion: MemAELoss, optimizer: optim.Optimizer, device: str) -> tuple:
64
+ """
65
+ Function to train the model on a single epoch for memory augmented autoencoder model. Returns the average training loss.
66
+ """
67
+
68
+ # Set the model on train mode
69
+ model.train()
70
+ running_total = 0.0
71
+ running_recon = 0.0
72
+ running_entropy = 0.0
73
+
74
+ # Iterate within dataloader
75
+ for tensors, labels in dataloader:
76
+ # Move the tensors and labels to device
77
+ tensors, labels = tensors.to(device), labels.to(device)
78
+
79
+ # Core 5-step optimization
80
+ optimizer.zero_grad(set_to_none=True) # set_to_none=True to optimize memory allocation
81
+ recon, attn = model(tensors)
82
+ loss, (recon_loss, entropy) = criterion(recon, tensors, attn)
83
+ loss.backward()
84
+ optimizer.step()
85
+
86
+ # Scale using batch size
87
+ bs = tensors.size(0)
88
+ running_total += loss.item() * bs
89
+ running_recon += recon_loss.item() * bs
90
+ running_entropy += entropy.item() * bs
91
+
92
+ n = len(dataloader.dataset)
93
+ return running_total / n, running_recon / n, running_entropy / n
94
+
95
+
96
+ def validate_memae(model: nn.Module, dataloader: DataLoader, criterion: MemAELoss, device: str) -> tuple:
97
+ """
98
+ Function to evaluate the memory augmented autoencoder model. Returns the average validation loss.
99
+ """
100
+
101
+ # Set the model on eval mode
102
+ model.eval()
103
+ running_total = 0.0
104
+ running_recon = 0.0
105
+ running_entropy = 0.0
106
+
107
+ # Deactivate the gradients for memory optimization
108
+ # NOTE: inference_mode eliminates gradient overhead, making it faster than no_grad but we'll need the tensors for the future so, no_grad is safer to use
109
+ with torch.no_grad():
110
+ # Iterate within dataloader
111
+ for tensors, labels in dataloader:
112
+ # Move the tensors and labels to device
113
+ tensors, labels = tensors.to(device), labels.to(device)
114
+
115
+ recon, attn = model(tensors)
116
+ loss, (recon_loss, entropy) = criterion(recon, tensors, attn)
117
+
118
+ # Scale using batch size
119
+ bs = tensors.size(0)
120
+ running_total += loss.item() * bs
121
+ running_recon += recon_loss.item() * bs
122
+ running_entropy += entropy.item() * bs
123
+
124
+ n = len(dataloader.dataset)
125
+ return running_total / n, running_recon / n, running_entropy / n
126
+
127
+
128
+ def train_one_epoch_pred(model: nn.Module, dataloader: DataLoader, criterion: PredictionLoss, optimizer: optim.Optimizer, device: str) -> tuple:
129
+ """
130
+ Function to train our prediction model.
131
+ """
132
+
133
+ model.train()
134
+ running_total = 0.0
135
+ running_intensity = 0.0
136
+ running_gradient = 0.0
137
+
138
+ for inputs, targets in dataloader:
139
+ inputs, targets = inputs.to(device), targets.to(device)
140
+
141
+ optimizer.zero_grad(set_to_none=True)
142
+ preds = model(inputs) # (B,1,H,W), single tensor
143
+ loss, (intensity, gradient) = criterion(preds, targets)
144
+ loss.backward()
145
+ optimizer.step()
146
+
147
+ bs = inputs.size(0)
148
+ running_total += loss.item() * bs
149
+ running_intensity += intensity.item() * bs
150
+ running_gradient += gradient.item() * bs
151
+
152
+ n = len(dataloader.dataset)
153
+ return running_total / n, running_intensity / n, running_gradient / n
154
+
155
+
156
+ def validate_pred(model: nn.Module, dataloader: DataLoader, criterion: PredictionLoss, device: str) -> tuple:
157
+ """
158
+ Function to evaluate the prediction model. Returns the average validation, intensity and gradient losses.
159
+ """
160
+
161
+ # Set the model on eval mode
162
+ model.eval()
163
+ running_total = 0.0
164
+ running_intensity = 0.0
165
+ running_gradient = 0.0
166
+
167
+ with torch.no_grad():
168
+ # Iterate within dataloader
169
+ for inputs, targets in dataloader:
170
+ # Move the tensors and labels to device
171
+ inputs, targets = inputs.to(device), targets.to(device)
172
+
173
+ preds = model(inputs)
174
+ loss, (intensity, gradient) = criterion(preds, targets)
175
+
176
+ # Scale using batch size
177
+ bs = inputs.size(0)
178
+ running_total += loss.item() * bs
179
+ running_intensity += intensity.item() * bs
180
+ running_gradient += gradient.item() * bs
181
+
182
+ n = len(dataloader.dataset)
183
+ return running_total / n, running_intensity / n, running_gradient / n
src/utils/__init__.py ADDED
File without changes
src/utils/config.py ADDED
File without changes
src/utils/logger.py ADDED
File without changes