Indrajit Ari commited on
Commit
dbced4f
·
0 Parent(s):

Initial commit — SegVision Video Segmentation App

Browse files

Full-stack AI video segmentation using:
- Backend: FastAPI + Celery + Redis + DeepLabV3-ResNet50
- Frontend: Next.js 14 + Tailwind CSS
- Output: H.264 MP4 with side-by-side original/segmented video

Setup:
Backend: pip install -r backend/requirements.txt
Frontend: cd frontend && npm install && npm run dev
Start all: bash start.sh

.gitignore ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ─── Python ───────────────────────────────────────────────────────────────────
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+ *.pyd
6
+ .Python
7
+ *.egg
8
+ *.egg-info/
9
+ dist/
10
+ build/
11
+ .eggs/
12
+
13
+ # Virtual envs
14
+ .venv/
15
+ venv/
16
+ env/
17
+ ENV/
18
+
19
+ # ─── Node / Next.js ───────────────────────────────────────────────────────────
20
+ node_modules/
21
+ .next/
22
+ out/
23
+ .npm
24
+ *.tsbuildinfo
25
+ next-env.d.ts
26
+
27
+ # ─── ML / Data ────────────────────────────────────────────────────────────────
28
+ *.pt
29
+ *.pth
30
+ *.onnx
31
+ *.h5
32
+ *.pkl
33
+ *.npy
34
+ *.npz
35
+ /tmp/
36
+ uploads/
37
+ outputs/
38
+
39
+ # ─── Environment / Secrets ────────────────────────────────────────────────────
40
+ .env
41
+ .env.local
42
+ .env.*.local
43
+ *.env
44
+
45
+ # ─── OS ───────────────────────────────────────────────────────────────────────
46
+ .DS_Store
47
+ .DS_Store?
48
+ ._*
49
+ Thumbs.db
50
+
51
+ # ─── Logs ─────────────────────────────────────────────────────────────────────
52
+ *.log
53
+ logs/
54
+ celery_worker.log
55
+
56
+ # ─── Docker ───────────────────────────────────────────────────────────────────
57
+ docker-compose.override.yml
58
+
59
+ # ─── IDE ──────────────────────────────────────────────────────────────────────
60
+ .idea/
61
+ .vscode/
62
+ *.swp
63
+ *.swo
README.md ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SegVision — AI Video Segmentation App
2
+
3
+ > Upload any video → get real-time semantic segmentation with 21-class PASCAL VOC overlay.
4
+ > Powered by **DeepLabV3 + ResNet-50**, **FastAPI**, **Celery**, **Redis**, and **Next.js 14**.
5
+
6
+ ---
7
+
8
+ ## Architecture
9
+
10
+ ```
11
+ ┌─────────────────┐ HTTP/WS ┌──────────────────┐
12
+ │ Next.js 14 │◄─────────────────►│ FastAPI │
13
+ │ (port 3000) │ upload/status │ (port 8000) │
14
+ │ Dark UI │ WS progress │ DeepLabV3 model │
15
+ └─────────────────┘ └────────┬─────────┘
16
+ │ Celery tasks
17
+ ┌──────────▼─────────┐
18
+ │ Redis │
19
+ │ (broker + backend) │
20
+ └──────────┬─────────┘
21
+
22
+ ┌──────────▼─────────┐
23
+ │ Celery Worker │
24
+ │ (GPU inference) │
25
+ └────────────────────┘
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Quick Start (Local Dev)
31
+
32
+ ### Prerequisites
33
+ - Python 3.10+
34
+ - Node.js 18+ (for frontend)
35
+ - Redis (or Docker to run Redis)
36
+ - Optional: CUDA-capable GPU
37
+
38
+ ### One-command start
39
+
40
+ ```bash
41
+ bash start.sh
42
+ ```
43
+
44
+ This will:
45
+ 1. Start Redis (via Docker if not installed locally)
46
+ 2. Create Python venv + install backend deps
47
+ 3. Start Celery worker
48
+ 4. Start FastAPI on `:8000`
49
+ 5. Start Next.js on `:3000`
50
+
51
+ Then open **http://localhost:3000** 🎉
52
+
53
+ ---
54
+
55
+ ## Manual Setup
56
+
57
+ ### Backend
58
+
59
+ ```bash
60
+ cd backend
61
+ python3 -m venv .venv
62
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
63
+ pip install -r requirements.txt
64
+
65
+ # Terminal 1 — API server
66
+ uvicorn main:app --reload --port 8000
67
+
68
+ # Terminal 2 — Celery worker
69
+ celery -A tasks worker --loglevel=info
70
+ ```
71
+
72
+ ### Frontend
73
+
74
+ ```bash
75
+ cd frontend
76
+ npm install
77
+ npm run dev
78
+ ```
79
+
80
+ ### Redis (if not installed)
81
+
82
+ ```bash
83
+ docker run -d -p 6379:6379 redis:7-alpine
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Docker (Production)
89
+
90
+ ```bash
91
+ docker-compose up --build
92
+ ```
93
+
94
+ Services:
95
+ | Service | Port | Description |
96
+ |---|---|---|
97
+ | `frontend` | 3000 | Next.js UI |
98
+ | `backend` | 8000 | FastAPI + model |
99
+ | `worker` | — | Celery inference worker |
100
+ | `redis` | 6379 | Message broker |
101
+
102
+ ---
103
+
104
+ ## API Reference
105
+
106
+ | Method | Endpoint | Description |
107
+ |---|---|---|
108
+ | `POST` | `/api/upload` | Upload video → returns `job_id` |
109
+ | `GET` | `/api/status/{job_id}` | Job progress (0–100%) + detected classes |
110
+ | `GET` | `/api/video/{job_id}` | Stream segmented MP4 |
111
+ | `DELETE` | `/api/job/{job_id}` | Cleanup files |
112
+ | `WS` | `/ws/{job_id}` | Real-time progress stream |
113
+ | `GET` | `/api/health` | Health check + device info |
114
+ | `GET` | `/docs` | Interactive Swagger UI |
115
+
116
+ ### Upload Response
117
+ ```json
118
+ {
119
+ "job_id": "uuid",
120
+ "status": "queued",
121
+ "filename": "my_video.mp4",
122
+ "size_mb": 12.5
123
+ }
124
+ ```
125
+
126
+ ### Status Response
127
+ ```json
128
+ {
129
+ "job_id": "uuid",
130
+ "status": "processing",
131
+ "pct": 42.7,
132
+ "detected": ["person", "car", "dog"]
133
+ }
134
+ ```
135
+
136
+ ---
137
+
138
+ ## PASCAL VOC Classes (21)
139
+
140
+ | ID | Class | Colour |
141
+ |---|---|---|
142
+ | 0 | background | ⬛ black |
143
+ | 1 | aeroplane | 🔵 sky blue |
144
+ | 2 | bicycle | 🟠 orange |
145
+ | 3 | bird | 🟡 gold |
146
+ | 4 | boat | 💙 deep sky blue |
147
+ | 5 | bottle | 🟣 dark violet |
148
+ | 6 | bus | 🩷 deep pink |
149
+ | 7 | car | 🔴 crimson |
150
+ | 8 | cat | 🟠 dark orange |
151
+ | 9 | chair | 🟤 saddle brown |
152
+ | 10 | cow | 🟡 yellow |
153
+ | 11 | diningtable | 🟤 chocolate |
154
+ | 12 | dog | 🟣 medium orchid |
155
+ | 13 | horse | 🩷 hot pink |
156
+ | 14 | motorbike | 🟢 spring green |
157
+ | 15 | person | 🔴 red-orange |
158
+ | 16 | potted plant | 🟢 forest green |
159
+ | 17 | sheep | 🟡 khaki |
160
+ | 18 | sofa | 🩵 dark turquoise |
161
+ | 19 | train | 🔵 blue |
162
+ | 20 | tv/monitor | 🩵 aquamarine |
163
+
164
+ ---
165
+
166
+ ## Performance Tips
167
+
168
+ - **GPU**: Set `DEVICE=cuda` — inference is ~10× faster
169
+ - **Video length**: Works best on clips ≤ 2 min (longer = queued async)
170
+ - **Resolution**: Frames are resized to max 640px — keeps quality + speed balanced
171
+ - **Workers**: Increase `--concurrency` in Celery for parallel jobs
172
+
173
+ ---
174
+
175
+ ## Project Structure
176
+
177
+ ```
178
+ video-seg-app/
179
+ ├── backend/
180
+ │ ├── inference.py # DeepLabV3 model + frame segmentation
181
+ │ ├── tasks.py # Celery task (async video processing)
182
+ │ ├── main.py # FastAPI: upload / status / video / WS
183
+ │ ├── requirements.txt
184
+ │ └── Dockerfile
185
+ ├── frontend/
186
+ │ ├── src/app/
187
+ │ │ ├── page.tsx # Upload UI (drag & drop)
188
+ │ │ ├── processing/[id]/ # Real-time progress page
189
+ │ │ └── result/[id]/ # Video player + class legend
190
+ │ ├── src/app/globals.css # Dark mode design system
191
+ │ ├── tailwind.config.js
192
+ │ └── Dockerfile
193
+ ├── docker-compose.yml
194
+ ├── start.sh # One-command local dev
195
+ └── README.md
196
+ ```
backend/Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # System deps for OpenCV
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ libgl1 libglib2.0-0 ffmpeg \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ COPY requirements.txt .
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ COPY . .
14
+
15
+ EXPOSE 8000
16
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
backend/__pycache__/inference.cpython-313.pyc ADDED
Binary file (12.9 kB). View file
 
backend/__pycache__/main.cpython-313.pyc ADDED
Binary file (11.3 kB). View file
 
backend/__pycache__/tasks.cpython-313.pyc ADDED
Binary file (2.44 kB). View file
 
backend/inference.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ inference.py — Video Segmentation Inference Engine
3
+ Extracted from U-Net + DeepLabV3 notebook.
4
+
5
+ Loads DeepLabV3-ResNet50 once at startup and exposes:
6
+ - segment_frame(frame_bgr) -> (seg_rgb, blend_bgr, detected_classes)
7
+ - process_video(input_path, output_path, progress_cb) -> None
8
+ """
9
+
10
+ import cv2
11
+ import numpy as np
12
+ import torch
13
+ import torch.nn.functional as F
14
+ from PIL import Image
15
+ from torchvision.models.segmentation import deeplabv3_resnet50, DeepLabV3_ResNet50_Weights
16
+ import warnings
17
+ import logging
18
+ import os
19
+ import subprocess
20
+ import tempfile
21
+
22
+ warnings.filterwarnings("ignore")
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # ─── PASCAL VOC 21 Classes ───────────────────────────────────────────────────
26
+
27
+ VOC_CLASSES = [
28
+ "background", "aeroplane", "bicycle", "bird", "boat",
29
+ "bottle", "bus", "car", "cat", "chair",
30
+ "cow", "diningtable", "dog", "horse", "motorbike",
31
+ "person", "potted plant", "sheep", "sofa", "train",
32
+ "tv/monitor",
33
+ ]
34
+
35
+ # Vibrant perceptually distinct colours (RGB)
36
+ PALETTE = np.array([
37
+ [ 0, 0, 0], # 0 background
38
+ [135, 206, 235], # 1 aeroplane
39
+ [255, 165, 0], # 2 bicycle
40
+ [255, 215, 0], # 3 bird
41
+ [ 0, 191, 255], # 4 boat
42
+ [148, 0, 211], # 5 bottle
43
+ [255, 20, 147], # 6 bus
44
+ [220, 20, 60], # 7 car
45
+ [255, 140, 0], # 8 cat
46
+ [139, 69, 19], # 9 chair
47
+ [255, 255, 0], # 10 cow
48
+ [210, 105, 30], # 11 dining table
49
+ [186, 85, 211], # 12 dog
50
+ [255, 105, 180], # 13 horse
51
+ [ 0, 255, 127], # 14 motorbike
52
+ [255, 69, 0], # 15 person
53
+ [ 34, 139, 34], # 16 potted plant
54
+ [240, 230, 140], # 17 sheep
55
+ [ 0, 206, 209], # 18 sofa
56
+ [ 0, 0, 255], # 19 train
57
+ [127, 255, 212], # 20 tv/monitor
58
+ ], dtype=np.uint8)
59
+
60
+ # ─── Model Singleton ─────────────────────────────────────────────────────────
61
+
62
+ _model = None
63
+ _preprocess = None
64
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
65
+
66
+
67
+ def get_ffmpeg() -> str:
68
+ """Return path to ffmpeg — uses bundled imageio-ffmpeg if system ffmpeg not found."""
69
+ import shutil
70
+ sys_ffmpeg = shutil.which("ffmpeg")
71
+ if sys_ffmpeg:
72
+ return sys_ffmpeg
73
+ try:
74
+ import imageio_ffmpeg
75
+ return imageio_ffmpeg.get_ffmpeg_exe()
76
+ except ImportError:
77
+ raise RuntimeError(
78
+ "ffmpeg not found. Install it: brew install ffmpeg "
79
+ "or: pip install imageio-ffmpeg"
80
+ )
81
+
82
+
83
+ def get_model():
84
+ """Load and cache the model (called once at startup)."""
85
+ global _model, _preprocess
86
+ if _model is None:
87
+ logger.info(f"Loading DeepLabV3-ResNet50 on {DEVICE}...")
88
+ weights = DeepLabV3_ResNet50_Weights.DEFAULT
89
+ _model = deeplabv3_resnet50(weights=weights).to(DEVICE)
90
+ _model.eval()
91
+ _preprocess = weights.transforms()
92
+ logger.info("Model loaded successfully.")
93
+ return _model, _preprocess
94
+
95
+
96
+ # ─── Core Inference Helpers ───────────────────────────────────────────────────
97
+
98
+ def decode_segmap(seg_mask: np.ndarray) -> np.ndarray:
99
+ """Convert (H,W) class index map → (H,W,3) RGB colour image."""
100
+ return PALETTE[seg_mask % len(PALETTE)]
101
+
102
+
103
+ def segment_frame(frame_bgr: np.ndarray, alpha: float = 0.55):
104
+ """
105
+ Segment a single BGR frame.
106
+
107
+ Returns:
108
+ seg_rgb : pure colour mask (H,W,3) uint8
109
+ blend_bgr : original blended with mask (H,W,3) uint8
110
+ detected : set of detected class IDs (excluding background)
111
+ """
112
+ model, preprocess = get_model()
113
+ h, w = frame_bgr.shape[:2]
114
+ frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB)
115
+ pil_img = Image.fromarray(frame_rgb)
116
+
117
+ inp = preprocess(pil_img).unsqueeze(0).to(DEVICE)
118
+
119
+ with torch.no_grad():
120
+ out = model(inp)["out"]
121
+ pred = out.argmax(dim=1).squeeze().cpu().numpy()
122
+
123
+ pred_resized = cv2.resize(
124
+ pred.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST
125
+ )
126
+
127
+ seg_rgb = decode_segmap(pred_resized)
128
+ seg_bgr = cv2.cvtColor(seg_rgb, cv2.COLOR_RGB2BGR)
129
+ blend_bgr = cv2.addWeighted(frame_bgr, 1 - alpha, seg_bgr, alpha, 0)
130
+ detected = set(np.unique(pred_resized).tolist()) - {0}
131
+
132
+ return seg_rgb, blend_bgr, detected
133
+
134
+
135
+ def make_legend_bar(class_ids: set, bar_w: int, bar_h: int = 40) -> np.ndarray:
136
+ """Render a colour legend strip for detected classes."""
137
+ bar = np.zeros((bar_h, bar_w, 3), dtype=np.uint8)
138
+ classes = sorted(class_ids)
139
+ if not classes:
140
+ return bar
141
+ sw = bar_w // max(len(classes), 1)
142
+ for i, cid in enumerate(classes):
143
+ x0, x1 = i * sw, min((i + 1) * sw, bar_w)
144
+ color = PALETTE[cid % len(PALETTE)].tolist()
145
+ bar[:, x0:x1] = color
146
+ label = VOC_CLASSES[cid] if cid < len(VOC_CLASSES) else str(cid)
147
+ cv2.putText(
148
+ bar, label, (x0 + 3, bar_h - 8),
149
+ cv2.FONT_HERSHEY_SIMPLEX, 0.38, (255, 255, 255), 1, cv2.LINE_AA,
150
+ )
151
+ return bar
152
+
153
+
154
+ # ─── Video Processing ─────────────────────────────────────────────────────────
155
+
156
+ def _reencode_h264(raw_path: str, final_path: str, fps: float):
157
+ """
158
+ Re-encode a raw opencv-written video to H.264 MP4 using ffmpeg.
159
+ H.264 is required for browser <video> playback.
160
+ """
161
+ ffmpeg = get_ffmpeg()
162
+ cmd = [
163
+ ffmpeg, "-y",
164
+ "-i", raw_path,
165
+ "-vcodec", "libx264",
166
+ "-preset", "fast",
167
+ "-crf", "23", # quality: 18=great, 28=ok; 23 is default
168
+ "-pix_fmt", "yuv420p", # required for QuickTime / Safari compatibility
169
+ "-movflags", "+faststart", # puts moov atom at start for streaming
170
+ "-an", # no audio track
171
+ final_path,
172
+ ]
173
+ logger.info(f"Re-encoding to H.264: {' '.join(cmd)}")
174
+ result = subprocess.run(cmd, capture_output=True, text=True)
175
+ if result.returncode != 0:
176
+ logger.error(f"ffmpeg error: {result.stderr[-500:]}")
177
+ raise RuntimeError(f"ffmpeg re-encoding failed: {result.stderr[-300:]}")
178
+ logger.info("H.264 re-encoding complete.")
179
+
180
+
181
+ def process_video(
182
+ input_path: str,
183
+ output_path: str,
184
+ progress_callback=None,
185
+ alpha: float = 0.55,
186
+ max_dim: int = 640,
187
+ ):
188
+ """
189
+ Process a video file frame-by-frame and write browser-compatible H.264 MP4.
190
+
191
+ Args:
192
+ input_path: path to input video
193
+ output_path: path to write final H.264 MP4 (browser-playable)
194
+ progress_callback: callable(pct: float, detected_names: list) or None
195
+ alpha: blend alpha for overlay (0=original, 1=mask)
196
+ max_dim: resize longest edge to this before inference (for speed)
197
+ """
198
+ cap = cv2.VideoCapture(input_path)
199
+ if not cap.isOpened():
200
+ raise ValueError(f"Cannot open video: {input_path}")
201
+
202
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
203
+ fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
204
+ orig_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
205
+ orig_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
206
+
207
+ # Resize to max_dim on longest edge (keeps aspect ratio)
208
+ scale = min(max_dim / orig_w, max_dim / orig_h, 1.0)
209
+ out_w = int(orig_w * scale)
210
+ out_h = int(orig_h * scale)
211
+
212
+ # H.264 requires even dimensions
213
+ out_w = out_w if out_w % 2 == 0 else out_w - 1
214
+ out_h = out_h if out_h % 2 == 0 else out_h - 1
215
+
216
+ combined_w = out_w * 2
217
+ combined_h = out_h + 44 # +44px for legend bar
218
+ # also ensure combined dims are even
219
+ combined_w = combined_w if combined_w % 2 == 0 else combined_w - 1
220
+ combined_h = combined_h if combined_h % 2 == 0 else combined_h - 1
221
+
222
+ # Write raw frames to a temp file first (mp4v is fastest for write)
223
+ # then re-encode to H.264 for browser compatibility
224
+ tmp_fd, tmp_path = tempfile.mkstemp(suffix="_raw.mp4")
225
+ os.close(tmp_fd)
226
+
227
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
228
+ writer = cv2.VideoWriter(tmp_path, fourcc, fps, (combined_w, combined_h))
229
+ if not writer.isOpened():
230
+ raise RuntimeError(f"Failed to open VideoWriter for {tmp_path}")
231
+
232
+ frame_idx = 0
233
+ all_detected = set()
234
+
235
+ logger.info(f"Processing {total_frames} frames @ {fps:.1f} fps — output {combined_w}x{combined_h}")
236
+
237
+ while True:
238
+ ret, frame = cap.read()
239
+ if not ret:
240
+ break
241
+
242
+ # Resize frame for inference
243
+ if scale < 1.0 or frame.shape[1] != out_w or frame.shape[0] != out_h:
244
+ frame = cv2.resize(frame, (out_w, out_h), interpolation=cv2.INTER_AREA)
245
+
246
+ seg_rgb, blend_bgr, detected = segment_frame(frame, alpha=alpha)
247
+ all_detected.update(detected)
248
+
249
+ # Legend bar (colour + label per class)
250
+ legend = make_legend_bar(all_detected, combined_w, bar_h=44)
251
+ legend_bgr = cv2.cvtColor(legend, cv2.COLOR_RGB2BGR)
252
+
253
+ # Side-by-side: original left | segmented overlay right
254
+ side_by_side = np.hstack([frame, blend_bgr])
255
+ combined = np.vstack([side_by_side, legend_bgr])
256
+
257
+ writer.write(combined)
258
+ frame_idx += 1
259
+
260
+ if progress_callback and total_frames > 0:
261
+ pct = round(frame_idx / total_frames * 100, 1)
262
+ detected_names = [
263
+ VOC_CLASSES[c] for c in sorted(all_detected) if c < len(VOC_CLASSES)
264
+ ]
265
+ progress_callback(pct, detected_names)
266
+
267
+ cap.release()
268
+ writer.release()
269
+ logger.info(f"Raw frames written to temp: {tmp_path}")
270
+
271
+ # Re-encode raw mp4v → H.264 for browser playback
272
+ try:
273
+ _reencode_h264(tmp_path, output_path, fps)
274
+ finally:
275
+ # Always clean up temp file
276
+ try:
277
+ os.unlink(tmp_path)
278
+ except OSError:
279
+ pass
280
+
281
+ logger.info(f"Final H.264 output: {output_path}")
282
+ return all_detected
backend/main.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ main.py — FastAPI backend for Video Segmentation App.
3
+
4
+ Endpoints:
5
+ POST /api/upload → Upload video, returns job_id
6
+ GET /api/status/{id} → Job status + progress
7
+ GET /api/video/{id} → Stream result video
8
+ WS /ws/{id} → WebSocket real-time progress
9
+ GET /api/health → Health check
10
+ """
11
+
12
+ import os
13
+ import uuid
14
+ import asyncio
15
+ import logging
16
+ from pathlib import Path
17
+ from typing import Optional
18
+
19
+ from fastapi import FastAPI, UploadFile, File, HTTPException, WebSocket, WebSocketDisconnect
20
+ from fastapi.middleware.cors import CORSMiddleware
21
+ from fastapi.responses import FileResponse, JSONResponse
22
+ from fastapi.staticfiles import StaticFiles
23
+ from celery.result import AsyncResult
24
+
25
+ from tasks import celery_app, segment_video_task
26
+ from inference import get_model # pre-load model at startup
27
+
28
+ # ─── Config ──────────────────────────────────────────────────────────────────
29
+
30
+ logging.basicConfig(level=logging.INFO)
31
+ logger = logging.getLogger(__name__)
32
+
33
+ UPLOAD_DIR = Path(os.getenv("UPLOAD_DIR", "/tmp/video_seg/uploads"))
34
+ OUTPUT_DIR = Path(os.getenv("OUTPUT_DIR", "/tmp/video_seg/outputs"))
35
+ MAX_FILE_SIZE_MB = int(os.getenv("MAX_FILE_SIZE_MB", "200"))
36
+ ALLOWED_EXTENSIONS = {".mp4", ".avi", ".mov", ".mkv", ".webm"}
37
+
38
+ UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
39
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
40
+
41
+ # ─── App ─────────────────────────────────────────────────────────────────────
42
+
43
+ app = FastAPI(
44
+ title="Video Segmentation API",
45
+ description="Upload a video and get semantic segmentation overlay",
46
+ version="1.0.0",
47
+ )
48
+
49
+ app.add_middleware(
50
+ CORSMiddleware,
51
+ allow_origins=["*"], # tighten in production
52
+ allow_credentials=True,
53
+ allow_methods=["*"],
54
+ allow_headers=["*"],
55
+ )
56
+
57
+ # ─── Startup: warm up the model ───────────────────────────────────────────────
58
+
59
+ @app.on_event("startup")
60
+ async def startup_event():
61
+ logger.info("Warming up segmentation model …")
62
+ get_model()
63
+ logger.info("Model ready.")
64
+
65
+
66
+ # ─── WebSocket connection manager ─────────────────────────────────────────────
67
+
68
+ class ConnectionManager:
69
+ def __init__(self):
70
+ self.active: dict[str, list[WebSocket]] = {}
71
+
72
+ async def connect(self, job_id: str, ws: WebSocket):
73
+ await ws.accept()
74
+ self.active.setdefault(job_id, []).append(ws)
75
+
76
+ def disconnect(self, job_id: str, ws: WebSocket):
77
+ if job_id in self.active:
78
+ self.active[job_id].discard(ws)
79
+
80
+ async def broadcast(self, job_id: str, data: dict):
81
+ for ws in list(self.active.get(job_id, [])):
82
+ try:
83
+ await ws.send_json(data)
84
+ except Exception:
85
+ self.active[job_id].discard(ws)
86
+
87
+
88
+ manager = ConnectionManager()
89
+
90
+
91
+ # ─── Endpoints ───────────────────────────────────────────────────────────────
92
+
93
+ @app.get("/api/health")
94
+ async def health():
95
+ return {"status": "ok", "device": "cuda" if _cuda_available() else "cpu"}
96
+
97
+
98
+ def _cuda_available():
99
+ try:
100
+ import torch
101
+ return torch.cuda.is_available()
102
+ except Exception:
103
+ return False
104
+
105
+
106
+ @app.post("/api/upload")
107
+ async def upload_video(file: UploadFile = File(...)):
108
+ """Accept video file, enqueue segmentation task, return job_id."""
109
+
110
+ # Validate extension
111
+ ext = Path(file.filename).suffix.lower()
112
+ if ext not in ALLOWED_EXTENSIONS:
113
+ raise HTTPException(
114
+ status_code=400,
115
+ detail=f"Unsupported format '{ext}'. Allowed: {ALLOWED_EXTENSIONS}",
116
+ )
117
+
118
+ job_id = str(uuid.uuid4())
119
+ input_path = UPLOAD_DIR / f"{job_id}{ext}"
120
+ output_path = OUTPUT_DIR / f"{job_id}_output.mp4"
121
+
122
+ # Stream write to disk
123
+ content = await file.read()
124
+ size_mb = len(content) / (1024 * 1024)
125
+ if size_mb > MAX_FILE_SIZE_MB:
126
+ raise HTTPException(
127
+ status_code=413,
128
+ detail=f"File too large ({size_mb:.1f} MB). Max: {MAX_FILE_SIZE_MB} MB",
129
+ )
130
+
131
+ with open(input_path, "wb") as f:
132
+ f.write(content)
133
+
134
+ logger.info(f"[{job_id}] Uploaded {file.filename} ({size_mb:.1f} MB)")
135
+
136
+ # Dispatch Celery task
137
+ task = segment_video_task.apply_async(
138
+ args=[job_id, str(input_path), str(output_path)],
139
+ task_id=job_id,
140
+ )
141
+
142
+ return {
143
+ "job_id": job_id,
144
+ "status": "queued",
145
+ "filename": file.filename,
146
+ "size_mb": round(size_mb, 2),
147
+ }
148
+
149
+
150
+ @app.get("/api/status/{job_id}")
151
+ async def get_status(job_id: str):
152
+ """Return current job status and progress."""
153
+ result = AsyncResult(job_id, app=celery_app)
154
+
155
+ state = result.state # PENDING / PROGRESS / SUCCESS / FAILURE
156
+
157
+ if state == "PENDING":
158
+ return {"job_id": job_id, "status": "queued", "pct": 0.0, "detected": []}
159
+
160
+ if state == "PROGRESS":
161
+ meta = result.info or {}
162
+ return {
163
+ "job_id": job_id,
164
+ "status": "processing",
165
+ "pct": meta.get("pct", 0.0),
166
+ "detected": meta.get("detected", []),
167
+ }
168
+
169
+ if state == "SUCCESS":
170
+ info = result.result or {}
171
+ return {
172
+ "job_id": job_id,
173
+ "status": "done",
174
+ "pct": 100.0,
175
+ "detected": info.get("detected", []),
176
+ }
177
+
178
+ if state == "FAILURE":
179
+ return {
180
+ "job_id": job_id,
181
+ "status": "error",
182
+ "error": str(result.info),
183
+ }
184
+
185
+ return {"job_id": job_id, "status": state.lower()}
186
+
187
+
188
+ @app.get("/api/video/{job_id}")
189
+ async def get_video(job_id: str):
190
+ """Stream the processed video file."""
191
+ output_path = OUTPUT_DIR / f"{job_id}_output.mp4"
192
+ if not output_path.exists():
193
+ raise HTTPException(status_code=404, detail="Result not ready yet")
194
+ return FileResponse(
195
+ str(output_path),
196
+ media_type="video/mp4",
197
+ filename=f"segmented_{job_id}.mp4",
198
+ )
199
+
200
+
201
+ @app.delete("/api/job/{job_id}")
202
+ async def delete_job(job_id: str):
203
+ """Cleanup uploaded + output files for a job."""
204
+ for path in UPLOAD_DIR.glob(f"{job_id}*"):
205
+ path.unlink(missing_ok=True)
206
+ for path in OUTPUT_DIR.glob(f"{job_id}*"):
207
+ path.unlink(missing_ok=True)
208
+ return {"job_id": job_id, "status": "deleted"}
209
+
210
+
211
+ # ─── WebSocket: real-time progress ────────────────────────────────────────────
212
+
213
+ @app.websocket("/ws/{job_id}")
214
+ async def websocket_progress(websocket: WebSocket, job_id: str):
215
+ """
216
+ Poll Celery job status and push updates to connected browser.
217
+ Closes automatically when job finishes.
218
+ """
219
+ await manager.connect(job_id, websocket)
220
+ try:
221
+ while True:
222
+ result = AsyncResult(job_id, app=celery_app)
223
+ state = result.state
224
+
225
+ if state == "PENDING":
226
+ payload = {"status": "queued", "pct": 0.0, "detected": []}
227
+ elif state == "PROGRESS":
228
+ meta = result.info or {}
229
+ payload = {
230
+ "status": "processing",
231
+ "pct": meta.get("pct", 0.0),
232
+ "detected": meta.get("detected", []),
233
+ }
234
+ elif state == "SUCCESS":
235
+ info = result.result or {}
236
+ payload = {
237
+ "status": "done",
238
+ "pct": 100.0,
239
+ "detected": info.get("detected", []),
240
+ }
241
+ await websocket.send_json(payload)
242
+ break # close WS on completion
243
+ elif state == "FAILURE":
244
+ payload = {"status": "error", "error": str(result.info)}
245
+ await websocket.send_json(payload)
246
+ break
247
+ else:
248
+ payload = {"status": state.lower(), "pct": 0.0}
249
+
250
+ await websocket.send_json(payload)
251
+ await asyncio.sleep(0.8) # poll every 800ms
252
+
253
+ except WebSocketDisconnect:
254
+ logger.info(f"[{job_id}] WebSocket disconnected")
255
+ finally:
256
+ manager.disconnect(job_id, websocket)
backend/requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.111.0
2
+ uvicorn[standard]>=0.29.0
3
+ python-multipart>=0.0.9
4
+ celery[redis]>=5.3.6
5
+ redis>=5.0.4
6
+ torch>=2.2.0
7
+ torchvision>=0.17.0
8
+ opencv-python-headless>=4.9.0
9
+ Pillow>=10.3.0
10
+ numpy>=1.26.0
11
+ imageio>=2.34.0
12
+ imageio-ffmpeg>=0.4.9
backend/tasks.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ tasks.py — Celery async tasks for video segmentation.
3
+ """
4
+
5
+ import os
6
+ import json
7
+ import logging
8
+ from celery import Celery
9
+ from inference import process_video, VOC_CLASSES
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/0")
14
+
15
+ celery_app = Celery(
16
+ "video_seg",
17
+ broker=REDIS_URL,
18
+ backend=REDIS_URL,
19
+ )
20
+
21
+ celery_app.conf.update(
22
+ task_serializer="json",
23
+ accept_content=["json"],
24
+ result_serializer="json",
25
+ timezone="UTC",
26
+ enable_utc=True,
27
+ task_track_started=True,
28
+ result_expires=3600, # results expire in 1 hour
29
+ )
30
+
31
+
32
+ @celery_app.task(bind=True, name="tasks.segment_video")
33
+ def segment_video_task(self, job_id: str, input_path: str, output_path: str):
34
+ """
35
+ Celery task: runs video segmentation and updates progress via Redis.
36
+ Progress is stored in Celery's backend so FastAPI can poll it.
37
+ """
38
+ try:
39
+ self.update_state(
40
+ state="PROGRESS",
41
+ meta={"pct": 0.0, "detected": [], "status": "starting"},
42
+ )
43
+
44
+ def on_progress(pct, detected_names):
45
+ self.update_state(
46
+ state="PROGRESS",
47
+ meta={
48
+ "pct": pct,
49
+ "detected": detected_names,
50
+ "status": "processing",
51
+ },
52
+ )
53
+
54
+ detected = process_video(
55
+ input_path=input_path,
56
+ output_path=output_path,
57
+ progress_callback=on_progress,
58
+ )
59
+
60
+ detected_names = [
61
+ VOC_CLASSES[c] for c in sorted(detected) if c < len(VOC_CLASSES)
62
+ ]
63
+
64
+ return {
65
+ "status": "done",
66
+ "pct": 100.0,
67
+ "detected": detected_names,
68
+ "output_path": output_path,
69
+ }
70
+
71
+ except Exception as exc:
72
+ self.update_state(
73
+ state="FAILURE",
74
+ meta={"status": "error", "error": str(exc)},
75
+ )
76
+ raise exc
docker-compose.yml ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.9'
2
+
3
+ services:
4
+
5
+ # ── Redis (message broker + result backend) ──────────────────────────────
6
+ redis:
7
+ image: redis:7-alpine
8
+ container_name: seg_redis
9
+ ports:
10
+ - "6379:6379"
11
+ volumes:
12
+ - redis_data:/data
13
+ healthcheck:
14
+ test: ["CMD", "redis-cli", "ping"]
15
+ interval: 5s
16
+ timeout: 3s
17
+ retries: 10
18
+ restart: unless-stopped
19
+
20
+ # ── FastAPI Backend ───────────────────────────────────────────────────────
21
+ backend:
22
+ build:
23
+ context: ./backend
24
+ dockerfile: Dockerfile
25
+ container_name: seg_backend
26
+ ports:
27
+ - "8000:8000"
28
+ environment:
29
+ - REDIS_URL=redis://redis:6379/0
30
+ - UPLOAD_DIR=/data/uploads
31
+ - OUTPUT_DIR=/data/outputs
32
+ - MAX_FILE_SIZE_MB=200
33
+ volumes:
34
+ - seg_data:/data
35
+ - ./backend:/app # hot-reload in dev
36
+ depends_on:
37
+ redis:
38
+ condition: service_healthy
39
+ command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
40
+ restart: unless-stopped
41
+
42
+ # ── Celery Worker ─────────────────────────────────────────────────────────
43
+ worker:
44
+ build:
45
+ context: ./backend
46
+ dockerfile: Dockerfile
47
+ container_name: seg_worker
48
+ environment:
49
+ - REDIS_URL=redis://redis:6379/0
50
+ - UPLOAD_DIR=/data/uploads
51
+ - OUTPUT_DIR=/data/outputs
52
+ volumes:
53
+ - seg_data:/data
54
+ - ./backend:/app
55
+ depends_on:
56
+ redis:
57
+ condition: service_healthy
58
+ # 2 concurrent workers; adjust --concurrency for more GPU jobs
59
+ command: celery -A tasks worker --loglevel=info --concurrency=2
60
+ restart: unless-stopped
61
+
62
+ # ── Next.js Frontend ─────────────────────────────────────────────────────
63
+ frontend:
64
+ build:
65
+ context: ./frontend
66
+ dockerfile: Dockerfile
67
+ container_name: seg_frontend
68
+ ports:
69
+ - "3000:3000"
70
+ environment:
71
+ - NEXT_PUBLIC_API_URL=http://localhost:8000
72
+ depends_on:
73
+ - backend
74
+ restart: unless-stopped
75
+
76
+ volumes:
77
+ redis_data:
78
+ seg_data:
frontend/.env.local ADDED
@@ -0,0 +1 @@
 
 
1
+ git NEXT_PUBLIC_API_URL=http://localhost:8000
frontend/Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-alpine AS deps
2
+ WORKDIR /app
3
+ COPY package.json ./
4
+ RUN npm install
5
+
6
+ FROM node:20-alpine AS builder
7
+ WORKDIR /app
8
+ COPY --from=deps /app/node_modules ./node_modules
9
+ COPY . .
10
+ RUN npm run build
11
+
12
+ FROM node:20-alpine AS runner
13
+ WORKDIR /app
14
+ ENV NODE_ENV production
15
+ COPY --from=builder /app/.next ./.next
16
+ COPY --from=builder /app/node_modules ./node_modules
17
+ COPY --from=builder /app/package.json ./package.json
18
+ EXPOSE 3000
19
+ CMD ["npm", "start"]
frontend/next-env.d.ts ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/basic-features/typescript for more information.
frontend/next.config.js ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ env: {
4
+ NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000',
5
+ },
6
+ // Allow streaming video from the backend
7
+ async headers() {
8
+ return [
9
+ {
10
+ source: '/api/:path*',
11
+ headers: [
12
+ { key: 'Access-Control-Allow-Origin', value: '*' },
13
+ ],
14
+ },
15
+ ]
16
+ },
17
+ }
18
+
19
+ module.exports = nextConfig
frontend/package-lock.json ADDED
@@ -0,0 +1,1632 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "video-seg-frontend",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "video-seg-frontend",
9
+ "version": "0.1.0",
10
+ "dependencies": {
11
+ "next": "14.2.3",
12
+ "react": "^18",
13
+ "react-dom": "^18"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^20",
17
+ "@types/react": "^18",
18
+ "@types/react-dom": "^18",
19
+ "autoprefixer": "^10.0.1",
20
+ "postcss": "^8",
21
+ "tailwindcss": "^3.3.0",
22
+ "typescript": "^5"
23
+ }
24
+ },
25
+ "node_modules/@alloc/quick-lru": {
26
+ "version": "5.2.0",
27
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
28
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
29
+ "dev": true,
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": ">=10"
33
+ },
34
+ "funding": {
35
+ "url": "https://github.com/sponsors/sindresorhus"
36
+ }
37
+ },
38
+ "node_modules/@jridgewell/gen-mapping": {
39
+ "version": "0.3.13",
40
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
41
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
42
+ "dev": true,
43
+ "license": "MIT",
44
+ "dependencies": {
45
+ "@jridgewell/sourcemap-codec": "^1.5.0",
46
+ "@jridgewell/trace-mapping": "^0.3.24"
47
+ }
48
+ },
49
+ "node_modules/@jridgewell/resolve-uri": {
50
+ "version": "3.1.2",
51
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
52
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
53
+ "dev": true,
54
+ "license": "MIT",
55
+ "engines": {
56
+ "node": ">=6.0.0"
57
+ }
58
+ },
59
+ "node_modules/@jridgewell/sourcemap-codec": {
60
+ "version": "1.5.5",
61
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
62
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
63
+ "dev": true,
64
+ "license": "MIT"
65
+ },
66
+ "node_modules/@jridgewell/trace-mapping": {
67
+ "version": "0.3.31",
68
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
69
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
70
+ "dev": true,
71
+ "license": "MIT",
72
+ "dependencies": {
73
+ "@jridgewell/resolve-uri": "^3.1.0",
74
+ "@jridgewell/sourcemap-codec": "^1.4.14"
75
+ }
76
+ },
77
+ "node_modules/@next/env": {
78
+ "version": "14.2.3",
79
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz",
80
+ "integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==",
81
+ "license": "MIT"
82
+ },
83
+ "node_modules/@next/swc-darwin-arm64": {
84
+ "version": "14.2.3",
85
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz",
86
+ "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==",
87
+ "cpu": [
88
+ "arm64"
89
+ ],
90
+ "license": "MIT",
91
+ "optional": true,
92
+ "os": [
93
+ "darwin"
94
+ ],
95
+ "engines": {
96
+ "node": ">= 10"
97
+ }
98
+ },
99
+ "node_modules/@next/swc-darwin-x64": {
100
+ "version": "14.2.3",
101
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz",
102
+ "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==",
103
+ "cpu": [
104
+ "x64"
105
+ ],
106
+ "license": "MIT",
107
+ "optional": true,
108
+ "os": [
109
+ "darwin"
110
+ ],
111
+ "engines": {
112
+ "node": ">= 10"
113
+ }
114
+ },
115
+ "node_modules/@next/swc-linux-arm64-gnu": {
116
+ "version": "14.2.3",
117
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz",
118
+ "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==",
119
+ "cpu": [
120
+ "arm64"
121
+ ],
122
+ "license": "MIT",
123
+ "optional": true,
124
+ "os": [
125
+ "linux"
126
+ ],
127
+ "engines": {
128
+ "node": ">= 10"
129
+ }
130
+ },
131
+ "node_modules/@next/swc-linux-arm64-musl": {
132
+ "version": "14.2.3",
133
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz",
134
+ "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==",
135
+ "cpu": [
136
+ "arm64"
137
+ ],
138
+ "license": "MIT",
139
+ "optional": true,
140
+ "os": [
141
+ "linux"
142
+ ],
143
+ "engines": {
144
+ "node": ">= 10"
145
+ }
146
+ },
147
+ "node_modules/@next/swc-linux-x64-gnu": {
148
+ "version": "14.2.3",
149
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz",
150
+ "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==",
151
+ "cpu": [
152
+ "x64"
153
+ ],
154
+ "license": "MIT",
155
+ "optional": true,
156
+ "os": [
157
+ "linux"
158
+ ],
159
+ "engines": {
160
+ "node": ">= 10"
161
+ }
162
+ },
163
+ "node_modules/@next/swc-linux-x64-musl": {
164
+ "version": "14.2.3",
165
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz",
166
+ "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==",
167
+ "cpu": [
168
+ "x64"
169
+ ],
170
+ "license": "MIT",
171
+ "optional": true,
172
+ "os": [
173
+ "linux"
174
+ ],
175
+ "engines": {
176
+ "node": ">= 10"
177
+ }
178
+ },
179
+ "node_modules/@next/swc-win32-arm64-msvc": {
180
+ "version": "14.2.3",
181
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz",
182
+ "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==",
183
+ "cpu": [
184
+ "arm64"
185
+ ],
186
+ "license": "MIT",
187
+ "optional": true,
188
+ "os": [
189
+ "win32"
190
+ ],
191
+ "engines": {
192
+ "node": ">= 10"
193
+ }
194
+ },
195
+ "node_modules/@next/swc-win32-ia32-msvc": {
196
+ "version": "14.2.3",
197
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz",
198
+ "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==",
199
+ "cpu": [
200
+ "ia32"
201
+ ],
202
+ "license": "MIT",
203
+ "optional": true,
204
+ "os": [
205
+ "win32"
206
+ ],
207
+ "engines": {
208
+ "node": ">= 10"
209
+ }
210
+ },
211
+ "node_modules/@next/swc-win32-x64-msvc": {
212
+ "version": "14.2.3",
213
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz",
214
+ "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==",
215
+ "cpu": [
216
+ "x64"
217
+ ],
218
+ "license": "MIT",
219
+ "optional": true,
220
+ "os": [
221
+ "win32"
222
+ ],
223
+ "engines": {
224
+ "node": ">= 10"
225
+ }
226
+ },
227
+ "node_modules/@nodelib/fs.scandir": {
228
+ "version": "2.1.5",
229
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
230
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
231
+ "dev": true,
232
+ "license": "MIT",
233
+ "dependencies": {
234
+ "@nodelib/fs.stat": "2.0.5",
235
+ "run-parallel": "^1.1.9"
236
+ },
237
+ "engines": {
238
+ "node": ">= 8"
239
+ }
240
+ },
241
+ "node_modules/@nodelib/fs.stat": {
242
+ "version": "2.0.5",
243
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
244
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
245
+ "dev": true,
246
+ "license": "MIT",
247
+ "engines": {
248
+ "node": ">= 8"
249
+ }
250
+ },
251
+ "node_modules/@nodelib/fs.walk": {
252
+ "version": "1.2.8",
253
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
254
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
255
+ "dev": true,
256
+ "license": "MIT",
257
+ "dependencies": {
258
+ "@nodelib/fs.scandir": "2.1.5",
259
+ "fastq": "^1.6.0"
260
+ },
261
+ "engines": {
262
+ "node": ">= 8"
263
+ }
264
+ },
265
+ "node_modules/@swc/counter": {
266
+ "version": "0.1.3",
267
+ "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
268
+ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
269
+ "license": "Apache-2.0"
270
+ },
271
+ "node_modules/@swc/helpers": {
272
+ "version": "0.5.5",
273
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
274
+ "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
275
+ "license": "Apache-2.0",
276
+ "dependencies": {
277
+ "@swc/counter": "^0.1.3",
278
+ "tslib": "^2.4.0"
279
+ }
280
+ },
281
+ "node_modules/@types/node": {
282
+ "version": "20.19.39",
283
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
284
+ "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
285
+ "dev": true,
286
+ "license": "MIT",
287
+ "dependencies": {
288
+ "undici-types": "~6.21.0"
289
+ }
290
+ },
291
+ "node_modules/@types/prop-types": {
292
+ "version": "15.7.15",
293
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
294
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
295
+ "dev": true,
296
+ "license": "MIT"
297
+ },
298
+ "node_modules/@types/react": {
299
+ "version": "18.3.28",
300
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
301
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
302
+ "dev": true,
303
+ "license": "MIT",
304
+ "dependencies": {
305
+ "@types/prop-types": "*",
306
+ "csstype": "^3.2.2"
307
+ }
308
+ },
309
+ "node_modules/@types/react-dom": {
310
+ "version": "18.3.7",
311
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
312
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
313
+ "dev": true,
314
+ "license": "MIT",
315
+ "peerDependencies": {
316
+ "@types/react": "^18.0.0"
317
+ }
318
+ },
319
+ "node_modules/any-promise": {
320
+ "version": "1.3.0",
321
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
322
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
323
+ "dev": true,
324
+ "license": "MIT"
325
+ },
326
+ "node_modules/anymatch": {
327
+ "version": "3.1.3",
328
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
329
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
330
+ "dev": true,
331
+ "license": "ISC",
332
+ "dependencies": {
333
+ "normalize-path": "^3.0.0",
334
+ "picomatch": "^2.0.4"
335
+ },
336
+ "engines": {
337
+ "node": ">= 8"
338
+ }
339
+ },
340
+ "node_modules/arg": {
341
+ "version": "5.0.2",
342
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
343
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
344
+ "dev": true,
345
+ "license": "MIT"
346
+ },
347
+ "node_modules/autoprefixer": {
348
+ "version": "10.5.0",
349
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz",
350
+ "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==",
351
+ "dev": true,
352
+ "funding": [
353
+ {
354
+ "type": "opencollective",
355
+ "url": "https://opencollective.com/postcss/"
356
+ },
357
+ {
358
+ "type": "tidelift",
359
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
360
+ },
361
+ {
362
+ "type": "github",
363
+ "url": "https://github.com/sponsors/ai"
364
+ }
365
+ ],
366
+ "license": "MIT",
367
+ "dependencies": {
368
+ "browserslist": "^4.28.2",
369
+ "caniuse-lite": "^1.0.30001787",
370
+ "fraction.js": "^5.3.4",
371
+ "picocolors": "^1.1.1",
372
+ "postcss-value-parser": "^4.2.0"
373
+ },
374
+ "bin": {
375
+ "autoprefixer": "bin/autoprefixer"
376
+ },
377
+ "engines": {
378
+ "node": "^10 || ^12 || >=14"
379
+ },
380
+ "peerDependencies": {
381
+ "postcss": "^8.1.0"
382
+ }
383
+ },
384
+ "node_modules/baseline-browser-mapping": {
385
+ "version": "2.10.20",
386
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.20.tgz",
387
+ "integrity": "sha512-1AaXxEPfXT+GvTBJFuy4yXVHWJBXa4OdbIebGN/wX5DlsIkU0+wzGnd2lOzokSk51d5LUmqjgBLRLlypLUqInQ==",
388
+ "dev": true,
389
+ "license": "Apache-2.0",
390
+ "bin": {
391
+ "baseline-browser-mapping": "dist/cli.cjs"
392
+ },
393
+ "engines": {
394
+ "node": ">=6.0.0"
395
+ }
396
+ },
397
+ "node_modules/binary-extensions": {
398
+ "version": "2.3.0",
399
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
400
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
401
+ "dev": true,
402
+ "license": "MIT",
403
+ "engines": {
404
+ "node": ">=8"
405
+ },
406
+ "funding": {
407
+ "url": "https://github.com/sponsors/sindresorhus"
408
+ }
409
+ },
410
+ "node_modules/braces": {
411
+ "version": "3.0.3",
412
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
413
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
414
+ "dev": true,
415
+ "license": "MIT",
416
+ "dependencies": {
417
+ "fill-range": "^7.1.1"
418
+ },
419
+ "engines": {
420
+ "node": ">=8"
421
+ }
422
+ },
423
+ "node_modules/browserslist": {
424
+ "version": "4.28.2",
425
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
426
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
427
+ "dev": true,
428
+ "funding": [
429
+ {
430
+ "type": "opencollective",
431
+ "url": "https://opencollective.com/browserslist"
432
+ },
433
+ {
434
+ "type": "tidelift",
435
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
436
+ },
437
+ {
438
+ "type": "github",
439
+ "url": "https://github.com/sponsors/ai"
440
+ }
441
+ ],
442
+ "license": "MIT",
443
+ "dependencies": {
444
+ "baseline-browser-mapping": "^2.10.12",
445
+ "caniuse-lite": "^1.0.30001782",
446
+ "electron-to-chromium": "^1.5.328",
447
+ "node-releases": "^2.0.36",
448
+ "update-browserslist-db": "^1.2.3"
449
+ },
450
+ "bin": {
451
+ "browserslist": "cli.js"
452
+ },
453
+ "engines": {
454
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
455
+ }
456
+ },
457
+ "node_modules/busboy": {
458
+ "version": "1.6.0",
459
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
460
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
461
+ "dependencies": {
462
+ "streamsearch": "^1.1.0"
463
+ },
464
+ "engines": {
465
+ "node": ">=10.16.0"
466
+ }
467
+ },
468
+ "node_modules/camelcase-css": {
469
+ "version": "2.0.1",
470
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
471
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
472
+ "dev": true,
473
+ "license": "MIT",
474
+ "engines": {
475
+ "node": ">= 6"
476
+ }
477
+ },
478
+ "node_modules/caniuse-lite": {
479
+ "version": "1.0.30001788",
480
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz",
481
+ "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==",
482
+ "funding": [
483
+ {
484
+ "type": "opencollective",
485
+ "url": "https://opencollective.com/browserslist"
486
+ },
487
+ {
488
+ "type": "tidelift",
489
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
490
+ },
491
+ {
492
+ "type": "github",
493
+ "url": "https://github.com/sponsors/ai"
494
+ }
495
+ ],
496
+ "license": "CC-BY-4.0"
497
+ },
498
+ "node_modules/chokidar": {
499
+ "version": "3.6.0",
500
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
501
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
502
+ "dev": true,
503
+ "license": "MIT",
504
+ "dependencies": {
505
+ "anymatch": "~3.1.2",
506
+ "braces": "~3.0.2",
507
+ "glob-parent": "~5.1.2",
508
+ "is-binary-path": "~2.1.0",
509
+ "is-glob": "~4.0.1",
510
+ "normalize-path": "~3.0.0",
511
+ "readdirp": "~3.6.0"
512
+ },
513
+ "engines": {
514
+ "node": ">= 8.10.0"
515
+ },
516
+ "funding": {
517
+ "url": "https://paulmillr.com/funding/"
518
+ },
519
+ "optionalDependencies": {
520
+ "fsevents": "~2.3.2"
521
+ }
522
+ },
523
+ "node_modules/chokidar/node_modules/glob-parent": {
524
+ "version": "5.1.2",
525
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
526
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
527
+ "dev": true,
528
+ "license": "ISC",
529
+ "dependencies": {
530
+ "is-glob": "^4.0.1"
531
+ },
532
+ "engines": {
533
+ "node": ">= 6"
534
+ }
535
+ },
536
+ "node_modules/client-only": {
537
+ "version": "0.0.1",
538
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
539
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
540
+ "license": "MIT"
541
+ },
542
+ "node_modules/commander": {
543
+ "version": "4.1.1",
544
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
545
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
546
+ "dev": true,
547
+ "license": "MIT",
548
+ "engines": {
549
+ "node": ">= 6"
550
+ }
551
+ },
552
+ "node_modules/cssesc": {
553
+ "version": "3.0.0",
554
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
555
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
556
+ "dev": true,
557
+ "license": "MIT",
558
+ "bin": {
559
+ "cssesc": "bin/cssesc"
560
+ },
561
+ "engines": {
562
+ "node": ">=4"
563
+ }
564
+ },
565
+ "node_modules/csstype": {
566
+ "version": "3.2.3",
567
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
568
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
569
+ "dev": true,
570
+ "license": "MIT"
571
+ },
572
+ "node_modules/didyoumean": {
573
+ "version": "1.2.2",
574
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
575
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
576
+ "dev": true,
577
+ "license": "Apache-2.0"
578
+ },
579
+ "node_modules/dlv": {
580
+ "version": "1.1.3",
581
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
582
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
583
+ "dev": true,
584
+ "license": "MIT"
585
+ },
586
+ "node_modules/electron-to-chromium": {
587
+ "version": "1.5.340",
588
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz",
589
+ "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==",
590
+ "dev": true,
591
+ "license": "ISC"
592
+ },
593
+ "node_modules/es-errors": {
594
+ "version": "1.3.0",
595
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
596
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
597
+ "dev": true,
598
+ "license": "MIT",
599
+ "engines": {
600
+ "node": ">= 0.4"
601
+ }
602
+ },
603
+ "node_modules/escalade": {
604
+ "version": "3.2.0",
605
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
606
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
607
+ "dev": true,
608
+ "license": "MIT",
609
+ "engines": {
610
+ "node": ">=6"
611
+ }
612
+ },
613
+ "node_modules/fast-glob": {
614
+ "version": "3.3.3",
615
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
616
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
617
+ "dev": true,
618
+ "license": "MIT",
619
+ "dependencies": {
620
+ "@nodelib/fs.stat": "^2.0.2",
621
+ "@nodelib/fs.walk": "^1.2.3",
622
+ "glob-parent": "^5.1.2",
623
+ "merge2": "^1.3.0",
624
+ "micromatch": "^4.0.8"
625
+ },
626
+ "engines": {
627
+ "node": ">=8.6.0"
628
+ }
629
+ },
630
+ "node_modules/fast-glob/node_modules/glob-parent": {
631
+ "version": "5.1.2",
632
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
633
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
634
+ "dev": true,
635
+ "license": "ISC",
636
+ "dependencies": {
637
+ "is-glob": "^4.0.1"
638
+ },
639
+ "engines": {
640
+ "node": ">= 6"
641
+ }
642
+ },
643
+ "node_modules/fastq": {
644
+ "version": "1.20.1",
645
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
646
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
647
+ "dev": true,
648
+ "license": "ISC",
649
+ "dependencies": {
650
+ "reusify": "^1.0.4"
651
+ }
652
+ },
653
+ "node_modules/fill-range": {
654
+ "version": "7.1.1",
655
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
656
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
657
+ "dev": true,
658
+ "license": "MIT",
659
+ "dependencies": {
660
+ "to-regex-range": "^5.0.1"
661
+ },
662
+ "engines": {
663
+ "node": ">=8"
664
+ }
665
+ },
666
+ "node_modules/fraction.js": {
667
+ "version": "5.3.4",
668
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
669
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
670
+ "dev": true,
671
+ "license": "MIT",
672
+ "engines": {
673
+ "node": "*"
674
+ },
675
+ "funding": {
676
+ "type": "github",
677
+ "url": "https://github.com/sponsors/rawify"
678
+ }
679
+ },
680
+ "node_modules/fsevents": {
681
+ "version": "2.3.3",
682
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
683
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
684
+ "dev": true,
685
+ "hasInstallScript": true,
686
+ "license": "MIT",
687
+ "optional": true,
688
+ "os": [
689
+ "darwin"
690
+ ],
691
+ "engines": {
692
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
693
+ }
694
+ },
695
+ "node_modules/function-bind": {
696
+ "version": "1.1.2",
697
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
698
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
699
+ "dev": true,
700
+ "license": "MIT",
701
+ "funding": {
702
+ "url": "https://github.com/sponsors/ljharb"
703
+ }
704
+ },
705
+ "node_modules/glob-parent": {
706
+ "version": "6.0.2",
707
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
708
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
709
+ "dev": true,
710
+ "license": "ISC",
711
+ "dependencies": {
712
+ "is-glob": "^4.0.3"
713
+ },
714
+ "engines": {
715
+ "node": ">=10.13.0"
716
+ }
717
+ },
718
+ "node_modules/graceful-fs": {
719
+ "version": "4.2.11",
720
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
721
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
722
+ "license": "ISC"
723
+ },
724
+ "node_modules/hasown": {
725
+ "version": "2.0.3",
726
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
727
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
728
+ "dev": true,
729
+ "license": "MIT",
730
+ "dependencies": {
731
+ "function-bind": "^1.1.2"
732
+ },
733
+ "engines": {
734
+ "node": ">= 0.4"
735
+ }
736
+ },
737
+ "node_modules/is-binary-path": {
738
+ "version": "2.1.0",
739
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
740
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
741
+ "dev": true,
742
+ "license": "MIT",
743
+ "dependencies": {
744
+ "binary-extensions": "^2.0.0"
745
+ },
746
+ "engines": {
747
+ "node": ">=8"
748
+ }
749
+ },
750
+ "node_modules/is-core-module": {
751
+ "version": "2.16.1",
752
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
753
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
754
+ "dev": true,
755
+ "license": "MIT",
756
+ "dependencies": {
757
+ "hasown": "^2.0.2"
758
+ },
759
+ "engines": {
760
+ "node": ">= 0.4"
761
+ },
762
+ "funding": {
763
+ "url": "https://github.com/sponsors/ljharb"
764
+ }
765
+ },
766
+ "node_modules/is-extglob": {
767
+ "version": "2.1.1",
768
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
769
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
770
+ "dev": true,
771
+ "license": "MIT",
772
+ "engines": {
773
+ "node": ">=0.10.0"
774
+ }
775
+ },
776
+ "node_modules/is-glob": {
777
+ "version": "4.0.3",
778
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
779
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
780
+ "dev": true,
781
+ "license": "MIT",
782
+ "dependencies": {
783
+ "is-extglob": "^2.1.1"
784
+ },
785
+ "engines": {
786
+ "node": ">=0.10.0"
787
+ }
788
+ },
789
+ "node_modules/is-number": {
790
+ "version": "7.0.0",
791
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
792
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
793
+ "dev": true,
794
+ "license": "MIT",
795
+ "engines": {
796
+ "node": ">=0.12.0"
797
+ }
798
+ },
799
+ "node_modules/jiti": {
800
+ "version": "1.21.7",
801
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
802
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
803
+ "dev": true,
804
+ "license": "MIT",
805
+ "bin": {
806
+ "jiti": "bin/jiti.js"
807
+ }
808
+ },
809
+ "node_modules/js-tokens": {
810
+ "version": "4.0.0",
811
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
812
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
813
+ "license": "MIT"
814
+ },
815
+ "node_modules/lilconfig": {
816
+ "version": "3.1.3",
817
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
818
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
819
+ "dev": true,
820
+ "license": "MIT",
821
+ "engines": {
822
+ "node": ">=14"
823
+ },
824
+ "funding": {
825
+ "url": "https://github.com/sponsors/antonk52"
826
+ }
827
+ },
828
+ "node_modules/lines-and-columns": {
829
+ "version": "1.2.4",
830
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
831
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
832
+ "dev": true,
833
+ "license": "MIT"
834
+ },
835
+ "node_modules/loose-envify": {
836
+ "version": "1.4.0",
837
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
838
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
839
+ "license": "MIT",
840
+ "dependencies": {
841
+ "js-tokens": "^3.0.0 || ^4.0.0"
842
+ },
843
+ "bin": {
844
+ "loose-envify": "cli.js"
845
+ }
846
+ },
847
+ "node_modules/merge2": {
848
+ "version": "1.4.1",
849
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
850
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
851
+ "dev": true,
852
+ "license": "MIT",
853
+ "engines": {
854
+ "node": ">= 8"
855
+ }
856
+ },
857
+ "node_modules/micromatch": {
858
+ "version": "4.0.8",
859
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
860
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
861
+ "dev": true,
862
+ "license": "MIT",
863
+ "dependencies": {
864
+ "braces": "^3.0.3",
865
+ "picomatch": "^2.3.1"
866
+ },
867
+ "engines": {
868
+ "node": ">=8.6"
869
+ }
870
+ },
871
+ "node_modules/mz": {
872
+ "version": "2.7.0",
873
+ "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
874
+ "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
875
+ "dev": true,
876
+ "license": "MIT",
877
+ "dependencies": {
878
+ "any-promise": "^1.0.0",
879
+ "object-assign": "^4.0.1",
880
+ "thenify-all": "^1.0.0"
881
+ }
882
+ },
883
+ "node_modules/nanoid": {
884
+ "version": "3.3.11",
885
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
886
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
887
+ "funding": [
888
+ {
889
+ "type": "github",
890
+ "url": "https://github.com/sponsors/ai"
891
+ }
892
+ ],
893
+ "license": "MIT",
894
+ "bin": {
895
+ "nanoid": "bin/nanoid.cjs"
896
+ },
897
+ "engines": {
898
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
899
+ }
900
+ },
901
+ "node_modules/next": {
902
+ "version": "14.2.3",
903
+ "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz",
904
+ "integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==",
905
+ "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.",
906
+ "license": "MIT",
907
+ "dependencies": {
908
+ "@next/env": "14.2.3",
909
+ "@swc/helpers": "0.5.5",
910
+ "busboy": "1.6.0",
911
+ "caniuse-lite": "^1.0.30001579",
912
+ "graceful-fs": "^4.2.11",
913
+ "postcss": "8.4.31",
914
+ "styled-jsx": "5.1.1"
915
+ },
916
+ "bin": {
917
+ "next": "dist/bin/next"
918
+ },
919
+ "engines": {
920
+ "node": ">=18.17.0"
921
+ },
922
+ "optionalDependencies": {
923
+ "@next/swc-darwin-arm64": "14.2.3",
924
+ "@next/swc-darwin-x64": "14.2.3",
925
+ "@next/swc-linux-arm64-gnu": "14.2.3",
926
+ "@next/swc-linux-arm64-musl": "14.2.3",
927
+ "@next/swc-linux-x64-gnu": "14.2.3",
928
+ "@next/swc-linux-x64-musl": "14.2.3",
929
+ "@next/swc-win32-arm64-msvc": "14.2.3",
930
+ "@next/swc-win32-ia32-msvc": "14.2.3",
931
+ "@next/swc-win32-x64-msvc": "14.2.3"
932
+ },
933
+ "peerDependencies": {
934
+ "@opentelemetry/api": "^1.1.0",
935
+ "@playwright/test": "^1.41.2",
936
+ "react": "^18.2.0",
937
+ "react-dom": "^18.2.0",
938
+ "sass": "^1.3.0"
939
+ },
940
+ "peerDependenciesMeta": {
941
+ "@opentelemetry/api": {
942
+ "optional": true
943
+ },
944
+ "@playwright/test": {
945
+ "optional": true
946
+ },
947
+ "sass": {
948
+ "optional": true
949
+ }
950
+ }
951
+ },
952
+ "node_modules/next/node_modules/postcss": {
953
+ "version": "8.4.31",
954
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
955
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
956
+ "funding": [
957
+ {
958
+ "type": "opencollective",
959
+ "url": "https://opencollective.com/postcss/"
960
+ },
961
+ {
962
+ "type": "tidelift",
963
+ "url": "https://tidelift.com/funding/github/npm/postcss"
964
+ },
965
+ {
966
+ "type": "github",
967
+ "url": "https://github.com/sponsors/ai"
968
+ }
969
+ ],
970
+ "license": "MIT",
971
+ "dependencies": {
972
+ "nanoid": "^3.3.6",
973
+ "picocolors": "^1.0.0",
974
+ "source-map-js": "^1.0.2"
975
+ },
976
+ "engines": {
977
+ "node": "^10 || ^12 || >=14"
978
+ }
979
+ },
980
+ "node_modules/node-releases": {
981
+ "version": "2.0.37",
982
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
983
+ "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
984
+ "dev": true,
985
+ "license": "MIT"
986
+ },
987
+ "node_modules/normalize-path": {
988
+ "version": "3.0.0",
989
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
990
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
991
+ "dev": true,
992
+ "license": "MIT",
993
+ "engines": {
994
+ "node": ">=0.10.0"
995
+ }
996
+ },
997
+ "node_modules/object-assign": {
998
+ "version": "4.1.1",
999
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1000
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1001
+ "dev": true,
1002
+ "license": "MIT",
1003
+ "engines": {
1004
+ "node": ">=0.10.0"
1005
+ }
1006
+ },
1007
+ "node_modules/object-hash": {
1008
+ "version": "3.0.0",
1009
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
1010
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
1011
+ "dev": true,
1012
+ "license": "MIT",
1013
+ "engines": {
1014
+ "node": ">= 6"
1015
+ }
1016
+ },
1017
+ "node_modules/path-parse": {
1018
+ "version": "1.0.7",
1019
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
1020
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
1021
+ "dev": true,
1022
+ "license": "MIT"
1023
+ },
1024
+ "node_modules/picocolors": {
1025
+ "version": "1.1.1",
1026
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1027
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1028
+ "license": "ISC"
1029
+ },
1030
+ "node_modules/picomatch": {
1031
+ "version": "2.3.2",
1032
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
1033
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
1034
+ "dev": true,
1035
+ "license": "MIT",
1036
+ "engines": {
1037
+ "node": ">=8.6"
1038
+ },
1039
+ "funding": {
1040
+ "url": "https://github.com/sponsors/jonschlinkert"
1041
+ }
1042
+ },
1043
+ "node_modules/pify": {
1044
+ "version": "2.3.0",
1045
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
1046
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
1047
+ "dev": true,
1048
+ "license": "MIT",
1049
+ "engines": {
1050
+ "node": ">=0.10.0"
1051
+ }
1052
+ },
1053
+ "node_modules/pirates": {
1054
+ "version": "4.0.7",
1055
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
1056
+ "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
1057
+ "dev": true,
1058
+ "license": "MIT",
1059
+ "engines": {
1060
+ "node": ">= 6"
1061
+ }
1062
+ },
1063
+ "node_modules/postcss": {
1064
+ "version": "8.5.10",
1065
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
1066
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
1067
+ "dev": true,
1068
+ "funding": [
1069
+ {
1070
+ "type": "opencollective",
1071
+ "url": "https://opencollective.com/postcss/"
1072
+ },
1073
+ {
1074
+ "type": "tidelift",
1075
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1076
+ },
1077
+ {
1078
+ "type": "github",
1079
+ "url": "https://github.com/sponsors/ai"
1080
+ }
1081
+ ],
1082
+ "license": "MIT",
1083
+ "dependencies": {
1084
+ "nanoid": "^3.3.11",
1085
+ "picocolors": "^1.1.1",
1086
+ "source-map-js": "^1.2.1"
1087
+ },
1088
+ "engines": {
1089
+ "node": "^10 || ^12 || >=14"
1090
+ }
1091
+ },
1092
+ "node_modules/postcss-import": {
1093
+ "version": "15.1.0",
1094
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
1095
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
1096
+ "dev": true,
1097
+ "license": "MIT",
1098
+ "dependencies": {
1099
+ "postcss-value-parser": "^4.0.0",
1100
+ "read-cache": "^1.0.0",
1101
+ "resolve": "^1.1.7"
1102
+ },
1103
+ "engines": {
1104
+ "node": ">=14.0.0"
1105
+ },
1106
+ "peerDependencies": {
1107
+ "postcss": "^8.0.0"
1108
+ }
1109
+ },
1110
+ "node_modules/postcss-js": {
1111
+ "version": "4.1.0",
1112
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz",
1113
+ "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==",
1114
+ "dev": true,
1115
+ "funding": [
1116
+ {
1117
+ "type": "opencollective",
1118
+ "url": "https://opencollective.com/postcss/"
1119
+ },
1120
+ {
1121
+ "type": "github",
1122
+ "url": "https://github.com/sponsors/ai"
1123
+ }
1124
+ ],
1125
+ "license": "MIT",
1126
+ "dependencies": {
1127
+ "camelcase-css": "^2.0.1"
1128
+ },
1129
+ "engines": {
1130
+ "node": "^12 || ^14 || >= 16"
1131
+ },
1132
+ "peerDependencies": {
1133
+ "postcss": "^8.4.21"
1134
+ }
1135
+ },
1136
+ "node_modules/postcss-load-config": {
1137
+ "version": "6.0.1",
1138
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz",
1139
+ "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==",
1140
+ "dev": true,
1141
+ "funding": [
1142
+ {
1143
+ "type": "opencollective",
1144
+ "url": "https://opencollective.com/postcss/"
1145
+ },
1146
+ {
1147
+ "type": "github",
1148
+ "url": "https://github.com/sponsors/ai"
1149
+ }
1150
+ ],
1151
+ "license": "MIT",
1152
+ "dependencies": {
1153
+ "lilconfig": "^3.1.1"
1154
+ },
1155
+ "engines": {
1156
+ "node": ">= 18"
1157
+ },
1158
+ "peerDependencies": {
1159
+ "jiti": ">=1.21.0",
1160
+ "postcss": ">=8.0.9",
1161
+ "tsx": "^4.8.1",
1162
+ "yaml": "^2.4.2"
1163
+ },
1164
+ "peerDependenciesMeta": {
1165
+ "jiti": {
1166
+ "optional": true
1167
+ },
1168
+ "postcss": {
1169
+ "optional": true
1170
+ },
1171
+ "tsx": {
1172
+ "optional": true
1173
+ },
1174
+ "yaml": {
1175
+ "optional": true
1176
+ }
1177
+ }
1178
+ },
1179
+ "node_modules/postcss-nested": {
1180
+ "version": "6.2.0",
1181
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
1182
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
1183
+ "dev": true,
1184
+ "funding": [
1185
+ {
1186
+ "type": "opencollective",
1187
+ "url": "https://opencollective.com/postcss/"
1188
+ },
1189
+ {
1190
+ "type": "github",
1191
+ "url": "https://github.com/sponsors/ai"
1192
+ }
1193
+ ],
1194
+ "license": "MIT",
1195
+ "dependencies": {
1196
+ "postcss-selector-parser": "^6.1.1"
1197
+ },
1198
+ "engines": {
1199
+ "node": ">=12.0"
1200
+ },
1201
+ "peerDependencies": {
1202
+ "postcss": "^8.2.14"
1203
+ }
1204
+ },
1205
+ "node_modules/postcss-selector-parser": {
1206
+ "version": "6.1.2",
1207
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
1208
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
1209
+ "dev": true,
1210
+ "license": "MIT",
1211
+ "dependencies": {
1212
+ "cssesc": "^3.0.0",
1213
+ "util-deprecate": "^1.0.2"
1214
+ },
1215
+ "engines": {
1216
+ "node": ">=4"
1217
+ }
1218
+ },
1219
+ "node_modules/postcss-value-parser": {
1220
+ "version": "4.2.0",
1221
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
1222
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
1223
+ "dev": true,
1224
+ "license": "MIT"
1225
+ },
1226
+ "node_modules/queue-microtask": {
1227
+ "version": "1.2.3",
1228
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
1229
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
1230
+ "dev": true,
1231
+ "funding": [
1232
+ {
1233
+ "type": "github",
1234
+ "url": "https://github.com/sponsors/feross"
1235
+ },
1236
+ {
1237
+ "type": "patreon",
1238
+ "url": "https://www.patreon.com/feross"
1239
+ },
1240
+ {
1241
+ "type": "consulting",
1242
+ "url": "https://feross.org/support"
1243
+ }
1244
+ ],
1245
+ "license": "MIT"
1246
+ },
1247
+ "node_modules/react": {
1248
+ "version": "18.3.1",
1249
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1250
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1251
+ "license": "MIT",
1252
+ "dependencies": {
1253
+ "loose-envify": "^1.1.0"
1254
+ },
1255
+ "engines": {
1256
+ "node": ">=0.10.0"
1257
+ }
1258
+ },
1259
+ "node_modules/react-dom": {
1260
+ "version": "18.3.1",
1261
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
1262
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
1263
+ "license": "MIT",
1264
+ "dependencies": {
1265
+ "loose-envify": "^1.1.0",
1266
+ "scheduler": "^0.23.2"
1267
+ },
1268
+ "peerDependencies": {
1269
+ "react": "^18.3.1"
1270
+ }
1271
+ },
1272
+ "node_modules/read-cache": {
1273
+ "version": "1.0.0",
1274
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
1275
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
1276
+ "dev": true,
1277
+ "license": "MIT",
1278
+ "dependencies": {
1279
+ "pify": "^2.3.0"
1280
+ }
1281
+ },
1282
+ "node_modules/readdirp": {
1283
+ "version": "3.6.0",
1284
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1285
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1286
+ "dev": true,
1287
+ "license": "MIT",
1288
+ "dependencies": {
1289
+ "picomatch": "^2.2.1"
1290
+ },
1291
+ "engines": {
1292
+ "node": ">=8.10.0"
1293
+ }
1294
+ },
1295
+ "node_modules/resolve": {
1296
+ "version": "1.22.12",
1297
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
1298
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
1299
+ "dev": true,
1300
+ "license": "MIT",
1301
+ "dependencies": {
1302
+ "es-errors": "^1.3.0",
1303
+ "is-core-module": "^2.16.1",
1304
+ "path-parse": "^1.0.7",
1305
+ "supports-preserve-symlinks-flag": "^1.0.0"
1306
+ },
1307
+ "bin": {
1308
+ "resolve": "bin/resolve"
1309
+ },
1310
+ "engines": {
1311
+ "node": ">= 0.4"
1312
+ },
1313
+ "funding": {
1314
+ "url": "https://github.com/sponsors/ljharb"
1315
+ }
1316
+ },
1317
+ "node_modules/reusify": {
1318
+ "version": "1.1.0",
1319
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
1320
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
1321
+ "dev": true,
1322
+ "license": "MIT",
1323
+ "engines": {
1324
+ "iojs": ">=1.0.0",
1325
+ "node": ">=0.10.0"
1326
+ }
1327
+ },
1328
+ "node_modules/run-parallel": {
1329
+ "version": "1.2.0",
1330
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
1331
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
1332
+ "dev": true,
1333
+ "funding": [
1334
+ {
1335
+ "type": "github",
1336
+ "url": "https://github.com/sponsors/feross"
1337
+ },
1338
+ {
1339
+ "type": "patreon",
1340
+ "url": "https://www.patreon.com/feross"
1341
+ },
1342
+ {
1343
+ "type": "consulting",
1344
+ "url": "https://feross.org/support"
1345
+ }
1346
+ ],
1347
+ "license": "MIT",
1348
+ "dependencies": {
1349
+ "queue-microtask": "^1.2.2"
1350
+ }
1351
+ },
1352
+ "node_modules/scheduler": {
1353
+ "version": "0.23.2",
1354
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
1355
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
1356
+ "license": "MIT",
1357
+ "dependencies": {
1358
+ "loose-envify": "^1.1.0"
1359
+ }
1360
+ },
1361
+ "node_modules/source-map-js": {
1362
+ "version": "1.2.1",
1363
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1364
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1365
+ "license": "BSD-3-Clause",
1366
+ "engines": {
1367
+ "node": ">=0.10.0"
1368
+ }
1369
+ },
1370
+ "node_modules/streamsearch": {
1371
+ "version": "1.1.0",
1372
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
1373
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
1374
+ "engines": {
1375
+ "node": ">=10.0.0"
1376
+ }
1377
+ },
1378
+ "node_modules/styled-jsx": {
1379
+ "version": "5.1.1",
1380
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
1381
+ "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
1382
+ "license": "MIT",
1383
+ "dependencies": {
1384
+ "client-only": "0.0.1"
1385
+ },
1386
+ "engines": {
1387
+ "node": ">= 12.0.0"
1388
+ },
1389
+ "peerDependencies": {
1390
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0"
1391
+ },
1392
+ "peerDependenciesMeta": {
1393
+ "@babel/core": {
1394
+ "optional": true
1395
+ },
1396
+ "babel-plugin-macros": {
1397
+ "optional": true
1398
+ }
1399
+ }
1400
+ },
1401
+ "node_modules/sucrase": {
1402
+ "version": "3.35.1",
1403
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz",
1404
+ "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==",
1405
+ "dev": true,
1406
+ "license": "MIT",
1407
+ "dependencies": {
1408
+ "@jridgewell/gen-mapping": "^0.3.2",
1409
+ "commander": "^4.0.0",
1410
+ "lines-and-columns": "^1.1.6",
1411
+ "mz": "^2.7.0",
1412
+ "pirates": "^4.0.1",
1413
+ "tinyglobby": "^0.2.11",
1414
+ "ts-interface-checker": "^0.1.9"
1415
+ },
1416
+ "bin": {
1417
+ "sucrase": "bin/sucrase",
1418
+ "sucrase-node": "bin/sucrase-node"
1419
+ },
1420
+ "engines": {
1421
+ "node": ">=16 || 14 >=14.17"
1422
+ }
1423
+ },
1424
+ "node_modules/supports-preserve-symlinks-flag": {
1425
+ "version": "1.0.0",
1426
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
1427
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
1428
+ "dev": true,
1429
+ "license": "MIT",
1430
+ "engines": {
1431
+ "node": ">= 0.4"
1432
+ },
1433
+ "funding": {
1434
+ "url": "https://github.com/sponsors/ljharb"
1435
+ }
1436
+ },
1437
+ "node_modules/tailwindcss": {
1438
+ "version": "3.4.19",
1439
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz",
1440
+ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
1441
+ "dev": true,
1442
+ "license": "MIT",
1443
+ "dependencies": {
1444
+ "@alloc/quick-lru": "^5.2.0",
1445
+ "arg": "^5.0.2",
1446
+ "chokidar": "^3.6.0",
1447
+ "didyoumean": "^1.2.2",
1448
+ "dlv": "^1.1.3",
1449
+ "fast-glob": "^3.3.2",
1450
+ "glob-parent": "^6.0.2",
1451
+ "is-glob": "^4.0.3",
1452
+ "jiti": "^1.21.7",
1453
+ "lilconfig": "^3.1.3",
1454
+ "micromatch": "^4.0.8",
1455
+ "normalize-path": "^3.0.0",
1456
+ "object-hash": "^3.0.0",
1457
+ "picocolors": "^1.1.1",
1458
+ "postcss": "^8.4.47",
1459
+ "postcss-import": "^15.1.0",
1460
+ "postcss-js": "^4.0.1",
1461
+ "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0",
1462
+ "postcss-nested": "^6.2.0",
1463
+ "postcss-selector-parser": "^6.1.2",
1464
+ "resolve": "^1.22.8",
1465
+ "sucrase": "^3.35.0"
1466
+ },
1467
+ "bin": {
1468
+ "tailwind": "lib/cli.js",
1469
+ "tailwindcss": "lib/cli.js"
1470
+ },
1471
+ "engines": {
1472
+ "node": ">=14.0.0"
1473
+ }
1474
+ },
1475
+ "node_modules/thenify": {
1476
+ "version": "3.3.1",
1477
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
1478
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
1479
+ "dev": true,
1480
+ "license": "MIT",
1481
+ "dependencies": {
1482
+ "any-promise": "^1.0.0"
1483
+ }
1484
+ },
1485
+ "node_modules/thenify-all": {
1486
+ "version": "1.6.0",
1487
+ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
1488
+ "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
1489
+ "dev": true,
1490
+ "license": "MIT",
1491
+ "dependencies": {
1492
+ "thenify": ">= 3.1.0 < 4"
1493
+ },
1494
+ "engines": {
1495
+ "node": ">=0.8"
1496
+ }
1497
+ },
1498
+ "node_modules/tinyglobby": {
1499
+ "version": "0.2.16",
1500
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
1501
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
1502
+ "dev": true,
1503
+ "license": "MIT",
1504
+ "dependencies": {
1505
+ "fdir": "^6.5.0",
1506
+ "picomatch": "^4.0.4"
1507
+ },
1508
+ "engines": {
1509
+ "node": ">=12.0.0"
1510
+ },
1511
+ "funding": {
1512
+ "url": "https://github.com/sponsors/SuperchupuDev"
1513
+ }
1514
+ },
1515
+ "node_modules/tinyglobby/node_modules/fdir": {
1516
+ "version": "6.5.0",
1517
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1518
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1519
+ "dev": true,
1520
+ "license": "MIT",
1521
+ "engines": {
1522
+ "node": ">=12.0.0"
1523
+ },
1524
+ "peerDependencies": {
1525
+ "picomatch": "^3 || ^4"
1526
+ },
1527
+ "peerDependenciesMeta": {
1528
+ "picomatch": {
1529
+ "optional": true
1530
+ }
1531
+ }
1532
+ },
1533
+ "node_modules/tinyglobby/node_modules/picomatch": {
1534
+ "version": "4.0.4",
1535
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
1536
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
1537
+ "dev": true,
1538
+ "license": "MIT",
1539
+ "engines": {
1540
+ "node": ">=12"
1541
+ },
1542
+ "funding": {
1543
+ "url": "https://github.com/sponsors/jonschlinkert"
1544
+ }
1545
+ },
1546
+ "node_modules/to-regex-range": {
1547
+ "version": "5.0.1",
1548
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1549
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1550
+ "dev": true,
1551
+ "license": "MIT",
1552
+ "dependencies": {
1553
+ "is-number": "^7.0.0"
1554
+ },
1555
+ "engines": {
1556
+ "node": ">=8.0"
1557
+ }
1558
+ },
1559
+ "node_modules/ts-interface-checker": {
1560
+ "version": "0.1.13",
1561
+ "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
1562
+ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
1563
+ "dev": true,
1564
+ "license": "Apache-2.0"
1565
+ },
1566
+ "node_modules/tslib": {
1567
+ "version": "2.8.1",
1568
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
1569
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
1570
+ "license": "0BSD"
1571
+ },
1572
+ "node_modules/typescript": {
1573
+ "version": "5.9.3",
1574
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
1575
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
1576
+ "dev": true,
1577
+ "license": "Apache-2.0",
1578
+ "bin": {
1579
+ "tsc": "bin/tsc",
1580
+ "tsserver": "bin/tsserver"
1581
+ },
1582
+ "engines": {
1583
+ "node": ">=14.17"
1584
+ }
1585
+ },
1586
+ "node_modules/undici-types": {
1587
+ "version": "6.21.0",
1588
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
1589
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
1590
+ "dev": true,
1591
+ "license": "MIT"
1592
+ },
1593
+ "node_modules/update-browserslist-db": {
1594
+ "version": "1.2.3",
1595
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
1596
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
1597
+ "dev": true,
1598
+ "funding": [
1599
+ {
1600
+ "type": "opencollective",
1601
+ "url": "https://opencollective.com/browserslist"
1602
+ },
1603
+ {
1604
+ "type": "tidelift",
1605
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1606
+ },
1607
+ {
1608
+ "type": "github",
1609
+ "url": "https://github.com/sponsors/ai"
1610
+ }
1611
+ ],
1612
+ "license": "MIT",
1613
+ "dependencies": {
1614
+ "escalade": "^3.2.0",
1615
+ "picocolors": "^1.1.1"
1616
+ },
1617
+ "bin": {
1618
+ "update-browserslist-db": "cli.js"
1619
+ },
1620
+ "peerDependencies": {
1621
+ "browserslist": ">= 4.21.0"
1622
+ }
1623
+ },
1624
+ "node_modules/util-deprecate": {
1625
+ "version": "1.0.2",
1626
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
1627
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
1628
+ "dev": true,
1629
+ "license": "MIT"
1630
+ }
1631
+ }
1632
+ }
frontend/package.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "video-seg-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev -p 3000",
7
+ "build": "next build",
8
+ "start": "next start"
9
+ },
10
+ "dependencies": {
11
+ "next": "14.2.3",
12
+ "react": "^18",
13
+ "react-dom": "^18"
14
+ },
15
+ "devDependencies": {
16
+ "@types/node": "^20",
17
+ "@types/react": "^18",
18
+ "@types/react-dom": "^18",
19
+ "autoprefixer": "^10.0.1",
20
+ "postcss": "^8",
21
+ "tailwindcss": "^3.3.0",
22
+ "typescript": "^5"
23
+ }
24
+ }
frontend/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/src/app/globals.css ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
2
+
3
+ @tailwind base;
4
+ @tailwind components;
5
+ @tailwind utilities;
6
+
7
+ :root {
8
+ --bg: #0f1117;
9
+ --surface: #1a1d27;
10
+ --border: #2a2d3a;
11
+ --brand: #6366f1;
12
+ --brand-light: #818cf8;
13
+ }
14
+
15
+ * {
16
+ box-sizing: border-box;
17
+ padding: 0;
18
+ margin: 0;
19
+ }
20
+
21
+ html, body {
22
+ min-height: 100vh;
23
+ background-color: var(--bg);
24
+ color: #fff;
25
+ font-family: 'Inter', system-ui, sans-serif;
26
+ scroll-behavior: smooth;
27
+ }
28
+
29
+ /* Custom scrollbar */
30
+ ::-webkit-scrollbar { width: 6px; }
31
+ ::-webkit-scrollbar-track { background: var(--bg); }
32
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
33
+ ::-webkit-scrollbar-thumb:hover { background: #3a3d4a; }
34
+
35
+ /* Glass card */
36
+ .glass {
37
+ background: rgba(26, 29, 39, 0.8);
38
+ backdrop-filter: blur(20px);
39
+ -webkit-backdrop-filter: blur(20px);
40
+ border: 1px solid rgba(255, 255, 255, 0.08);
41
+ }
42
+
43
+ /* Glow button */
44
+ .btn-glow {
45
+ position: relative;
46
+ overflow: hidden;
47
+ transition: all 0.3s ease;
48
+ }
49
+ .btn-glow::before {
50
+ content: '';
51
+ position: absolute;
52
+ inset: -2px;
53
+ border-radius: inherit;
54
+ background: linear-gradient(135deg, #6366f1, #a855f7, #06b6d4);
55
+ opacity: 0;
56
+ transition: opacity 0.3s;
57
+ z-index: -1;
58
+ }
59
+ .btn-glow:hover::before { opacity: 1; }
60
+
61
+ /* Shimmer skeleton */
62
+ .shimmer {
63
+ background: linear-gradient(
64
+ 90deg,
65
+ rgba(255,255,255,0.03) 25%,
66
+ rgba(255,255,255,0.08) 50%,
67
+ rgba(255,255,255,0.03) 75%
68
+ );
69
+ background-size: 200% 100%;
70
+ animation: shimmer 2s linear infinite;
71
+ }
72
+
73
+ /* Drop zone */
74
+ .drop-zone {
75
+ border: 2px dashed rgba(99, 102, 241, 0.4);
76
+ transition: all 0.25s ease;
77
+ }
78
+ .drop-zone:hover,
79
+ .drop-zone.drag-over {
80
+ border-color: rgba(99, 102, 241, 0.9);
81
+ background: rgba(99, 102, 241, 0.06);
82
+ box-shadow: 0 0 40px rgba(99, 102, 241, 0.12);
83
+ }
84
+
85
+ /* Progress bar */
86
+ .progress-track {
87
+ background: rgba(255,255,255,0.06);
88
+ border-radius: 999px;
89
+ overflow: hidden;
90
+ }
91
+ .progress-fill {
92
+ height: 100%;
93
+ border-radius: 999px;
94
+ background: linear-gradient(90deg, #6366f1, #a855f7);
95
+ transition: width 0.5s ease-out;
96
+ position: relative;
97
+ overflow: hidden;
98
+ }
99
+ .progress-fill::after {
100
+ content: '';
101
+ position: absolute;
102
+ top: 0; right: -100%; bottom: 0;
103
+ width: 60%;
104
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
105
+ animation: shimmer 1.5s linear infinite;
106
+ }
107
+
108
+ /* Class pill */
109
+ .class-pill {
110
+ display: inline-flex;
111
+ align-items: center;
112
+ gap: 6px;
113
+ padding: 3px 10px;
114
+ border-radius: 999px;
115
+ font-size: 12px;
116
+ font-weight: 500;
117
+ background: rgba(255,255,255,0.07);
118
+ border: 1px solid rgba(255,255,255,0.1);
119
+ animation: slideUp 0.3s ease-out;
120
+ }
121
+
122
+ /* Video comparison */
123
+ .video-wrapper {
124
+ position: relative;
125
+ overflow: hidden;
126
+ border-radius: 12px;
127
+ background: #000;
128
+ }
129
+ .video-wrapper video {
130
+ width: 100%;
131
+ height: 100%;
132
+ object-fit: contain;
133
+ display: block;
134
+ }
135
+
136
+ /* Stat card */
137
+ .stat-card {
138
+ background: rgba(255,255,255,0.04);
139
+ border: 1px solid rgba(255,255,255,0.08);
140
+ border-radius: 12px;
141
+ padding: 16px 20px;
142
+ transition: all 0.2s;
143
+ }
144
+ .stat-card:hover {
145
+ background: rgba(255,255,255,0.07);
146
+ border-color: rgba(99, 102, 241, 0.4);
147
+ }
frontend/src/app/layout.tsx ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from 'next'
2
+ import { Inter } from 'next/font/google'
3
+ import './globals.css'
4
+
5
+ const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
6
+
7
+ export const metadata: Metadata = {
8
+ title: 'SegVision — AI Video Segmentation',
9
+ description:
10
+ 'Upload any video and get real-time semantic segmentation with 21-class PASCAL VOC overlay. Powered by DeepLabV3 + ResNet-50.',
11
+ keywords: ['video segmentation', 'AI', 'semantic segmentation', 'DeepLabV3', 'computer vision'],
12
+ openGraph: {
13
+ title: 'SegVision — AI Video Segmentation',
14
+ description: 'Semantic segmentation overlay for any video, in seconds.',
15
+ type: 'website',
16
+ },
17
+ }
18
+
19
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
20
+ return (
21
+ <html lang="en" className={inter.variable}>
22
+ <head>
23
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
24
+ </head>
25
+ <body className="bg-surface text-white antialiased min-h-screen">
26
+ {/* Ambient background glow */}
27
+ <div className="fixed inset-0 pointer-events-none overflow-hidden">
28
+ <div className="absolute -top-40 -left-40 w-96 h-96 bg-brand-600/20 rounded-full blur-3xl animate-pulse-slow" />
29
+ <div className="absolute -bottom-40 -right-40 w-96 h-96 bg-purple-600/15 rounded-full blur-3xl animate-pulse-slow" style={{ animationDelay: '1.5s' }} />
30
+ </div>
31
+
32
+ {/* Navbar */}
33
+ <nav className="relative z-10 border-b border-surface-border bg-surface/80 backdrop-blur-xl">
34
+ <div className="max-w-6xl mx-auto px-4 h-16 flex items-center justify-between">
35
+ <a href="/" className="flex items-center gap-2 group">
36
+ <div className="w-8 h-8 rounded-lg bg-gradient-to-br from-brand-500 to-purple-600 flex items-center justify-center shadow-lg group-hover:scale-110 transition-transform">
37
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5">
38
+ <polygon points="5 3 19 12 5 21 5 3"/>
39
+ </svg>
40
+ </div>
41
+ <span className="text-lg font-bold tracking-tight">
42
+ Seg<span className="text-brand-400">Vision</span>
43
+ </span>
44
+ </a>
45
+ <div className="flex items-center gap-4 text-sm text-gray-400">
46
+ <span className="hidden sm:flex items-center gap-1.5">
47
+ <span className="w-2 h-2 rounded-full bg-green-400 animate-pulse"></span>
48
+ DeepLabV3 · ResNet-50 · PASCAL VOC 21
49
+ </span>
50
+ </div>
51
+ </div>
52
+ </nav>
53
+
54
+ <main className="relative z-10">
55
+ {children}
56
+ </main>
57
+
58
+ <footer className="relative z-10 border-t border-surface-border mt-20 py-8 text-center text-sm text-gray-500">
59
+ <p>SegVision · Semantic Video Segmentation · DeepLabV3 + ResNet-50</p>
60
+ </footer>
61
+ </body>
62
+ </html>
63
+ )
64
+ }
frontend/src/app/page.tsx ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useState, useRef, useCallback, DragEvent, ChangeEvent } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+
6
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8000'
7
+
8
+ const VOC_COLORS: Record<string, string> = {
9
+ background: '#000000', aeroplane: '#87CEEB', bicycle: '#FFA500',
10
+ bird: '#FFD700', boat: '#00BFFF', bottle: '#9400D3',
11
+ bus: '#FF1493', car: '#DC143C', cat: '#FF8C00',
12
+ chair: '#8B4513', cow: '#FFFF00', diningtable: '#D2691E',
13
+ dog: '#BA55D3', horse: '#FF69B4', motorbike: '#00FF7F',
14
+ person: '#FF4500', 'potted plant': '#228B22', sheep: '#F0E68C',
15
+ sofa: '#00CED1', train: '#0000FF', 'tv/monitor': '#7FFFD4',
16
+ }
17
+
18
+ const formatBytes = (bytes: number) => {
19
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
20
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
21
+ }
22
+
23
+ export default function HomePage() {
24
+ const router = useRouter()
25
+ const fileInputRef = useRef<HTMLInputElement>(null)
26
+ const [dragging, setDragging] = useState(false)
27
+ const [file, setFile] = useState<File | null>(null)
28
+ const [preview, setPreview] = useState<string | null>(null)
29
+ const [uploading, setUploading] = useState(false)
30
+ const [error, setError] = useState<string | null>(null)
31
+
32
+ const validate = (f: File): string | null => {
33
+ const allowed = ['video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/webm', 'video/x-matroska']
34
+ if (!allowed.includes(f.type) && !f.name.match(/\.(mp4|mov|avi|webm|mkv)$/i))
35
+ return 'Only MP4, MOV, AVI, WebM, MKV files are supported.'
36
+ if (f.size > 200 * 1024 * 1024)
37
+ return 'File too large. Maximum size is 200 MB.'
38
+ return null
39
+ }
40
+
41
+ const selectFile = useCallback((f: File) => {
42
+ const err = validate(f)
43
+ if (err) { setError(err); return }
44
+ setError(null)
45
+ setFile(f)
46
+ // Create video preview thumbnail
47
+ const url = URL.createObjectURL(f)
48
+ setPreview(url)
49
+ }, [])
50
+
51
+ const onDrop = useCallback((e: DragEvent<HTMLDivElement>) => {
52
+ e.preventDefault()
53
+ setDragging(false)
54
+ const f = e.dataTransfer.files[0]
55
+ if (f) selectFile(f)
56
+ }, [selectFile])
57
+
58
+ const onFileChange = (e: ChangeEvent<HTMLInputElement>) => {
59
+ const f = e.target.files?.[0]
60
+ if (f) selectFile(f)
61
+ }
62
+
63
+ const handleUpload = async () => {
64
+ if (!file) return
65
+ setUploading(true)
66
+ setError(null)
67
+ try {
68
+ const form = new FormData()
69
+ form.append('file', file)
70
+ const res = await fetch(`${API_BASE}/api/upload`, { method: 'POST', body: form })
71
+ if (!res.ok) {
72
+ const data = await res.json()
73
+ throw new Error(data.detail ?? 'Upload failed')
74
+ }
75
+ const data = await res.json()
76
+ router.push(`/processing/${data.job_id}`)
77
+ } catch (e: any) {
78
+ setError(e.message ?? 'Upload failed. Is the backend running?')
79
+ setUploading(false)
80
+ }
81
+ }
82
+
83
+ return (
84
+ <div className="max-w-4xl mx-auto px-4 py-16">
85
+
86
+ {/* Hero */}
87
+ <div className="text-center mb-14 animate-fade-in">
88
+ <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full text-xs font-medium bg-brand-500/10 border border-brand-500/20 text-brand-400 mb-6">
89
+ <span className="w-1.5 h-1.5 rounded-full bg-brand-400 animate-pulse"></span>
90
+ Powered by DeepLabV3 · ResNet-50 · PASCAL VOC
91
+ </div>
92
+ <h1 className="text-5xl sm:text-6xl font-extrabold tracking-tight mb-5 leading-tight">
93
+ AI Video
94
+ <span className="block bg-gradient-to-r from-brand-400 via-purple-400 to-cyan-400 bg-clip-text text-transparent">
95
+ Segmentation
96
+ </span>
97
+ </h1>
98
+ <p className="text-lg text-gray-400 max-w-xl mx-auto leading-relaxed">
99
+ Upload any video and watch AI detect and colour every object in real-time.
100
+ Get a side-by-side comparison instantly.
101
+ </p>
102
+ </div>
103
+
104
+ {/* Upload Card */}
105
+ <div className="glass rounded-2xl p-8 shadow-2xl animate-slide-up">
106
+
107
+ {!file ? (
108
+ /* Drop Zone */
109
+ <div
110
+ className={`drop-zone rounded-xl p-12 flex flex-col items-center justify-center cursor-pointer min-h-[280px] ${dragging ? 'drag-over' : ''}`}
111
+ onDragOver={(e) => { e.preventDefault(); setDragging(true) }}
112
+ onDragLeave={() => setDragging(false)}
113
+ onDrop={onDrop}
114
+ onClick={() => fileInputRef.current?.click()}
115
+ >
116
+ <div className={`w-20 h-20 rounded-2xl flex items-center justify-center mb-5 transition-all duration-300 ${dragging ? 'bg-brand-500/20 scale-110' : 'bg-brand-500/10'}`}>
117
+ <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke={dragging ? '#818cf8' : '#6366f1'} strokeWidth="1.5">
118
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
119
+ <polyline points="17 8 12 3 7 8"/>
120
+ <line x1="12" y1="3" x2="12" y2="15"/>
121
+ </svg>
122
+ </div>
123
+ <p className="text-xl font-semibold text-white mb-2">
124
+ {dragging ? 'Drop it here!' : 'Drop your video here'}
125
+ </p>
126
+ <p className="text-gray-400 text-sm mb-5">or click to browse your files</p>
127
+ <div className="flex items-center gap-2 text-xs text-gray-500">
128
+ <span className="px-2 py-0.5 rounded bg-white/5 border border-white/10">MP4</span>
129
+ <span className="px-2 py-0.5 rounded bg-white/5 border border-white/10">MOV</span>
130
+ <span className="px-2 py-0.5 rounded bg-white/5 border border-white/10">AVI</span>
131
+ <span className="px-2 py-0.5 rounded bg-white/5 border border-white/10">WebM</span>
132
+ <span className="text-gray-600">· Max 200 MB</span>
133
+ </div>
134
+ </div>
135
+ ) : (
136
+ /* File Preview */
137
+ <div className="animate-fade-in">
138
+ <div className="video-wrapper mb-5 max-h-64">
139
+ <video src={preview!} muted className="w-full max-h-64" controls />
140
+ </div>
141
+ <div className="flex items-center justify-between p-4 rounded-xl bg-white/5 border border-white/10 mb-5">
142
+ <div className="flex items-center gap-3">
143
+ <div className="w-10 h-10 rounded-lg bg-brand-500/15 flex items-center justify-center">
144
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="2">
145
+ <polygon points="5 3 19 12 5 21 5 3"/>
146
+ </svg>
147
+ </div>
148
+ <div>
149
+ <p className="font-medium text-white text-sm truncate max-w-[200px] sm:max-w-sm">{file.name}</p>
150
+ <p className="text-xs text-gray-400">{formatBytes(file.size)}</p>
151
+ </div>
152
+ </div>
153
+ <button
154
+ onClick={() => { setFile(null); setPreview(null); setError(null) }}
155
+ className="text-gray-400 hover:text-red-400 transition-colors p-1.5 rounded-lg hover:bg-red-500/10"
156
+ >
157
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
158
+ <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
159
+ </svg>
160
+ </button>
161
+ </div>
162
+ </div>
163
+ )}
164
+
165
+ <input ref={fileInputRef} type="file" accept="video/*" className="hidden" onChange={onFileChange} />
166
+
167
+ {error && (
168
+ <div className="mt-4 p-3 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400 text-sm flex items-center gap-2">
169
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
170
+ <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
171
+ </svg>
172
+ {error}
173
+ </div>
174
+ )}
175
+
176
+ <button
177
+ onClick={handleUpload}
178
+ disabled={!file || uploading}
179
+ className="mt-6 w-full py-4 rounded-xl font-semibold text-white text-base transition-all duration-200
180
+ bg-gradient-to-r from-brand-600 to-purple-600
181
+ hover:from-brand-500 hover:to-purple-500
182
+ disabled:opacity-40 disabled:cursor-not-allowed
183
+ hover:shadow-lg hover:shadow-brand-500/25 hover:-translate-y-0.5
184
+ active:translate-y-0 flex items-center justify-center gap-3"
185
+ >
186
+ {uploading ? (
187
+ <>
188
+ <svg className="animate-spin" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2">
189
+ <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
190
+ </svg>
191
+ Uploading …
192
+ </>
193
+ ) : (
194
+ <>
195
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2">
196
+ <polygon points="5 3 19 12 5 21 5 3"/>
197
+ </svg>
198
+ Segment Video
199
+ </>
200
+ )}
201
+ </button>
202
+ </div>
203
+
204
+ {/* Feature Cards */}
205
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-10 animate-fade-in">
206
+ {[
207
+ { icon: '🎯', title: '21 Object Classes', desc: 'People, cars, animals, furniture & more from PASCAL VOC' },
208
+ { icon: '⚡', title: 'GPU Accelerated', desc: 'CUDA inference for fast frame-by-frame processing' },
209
+ { icon: '🎬', title: 'Side-by-Side View', desc: 'Original vs segmented video with downloadable output' },
210
+ ].map((f) => (
211
+ <div key={f.title} className="stat-card">
212
+ <div className="text-2xl mb-2">{f.icon}</div>
213
+ <h3 className="font-semibold text-white text-sm mb-1">{f.title}</h3>
214
+ <p className="text-xs text-gray-400 leading-relaxed">{f.desc}</p>
215
+ </div>
216
+ ))}
217
+ </div>
218
+
219
+ {/* Class palette preview */}
220
+ <div className="mt-10 glass rounded-xl p-6 animate-fade-in">
221
+ <h2 className="text-sm font-semibold text-gray-300 mb-4">Detectable Classes</h2>
222
+ <div className="flex flex-wrap gap-2">
223
+ {Object.entries(VOC_COLORS).filter(([k]) => k !== 'background').map(([cls, hex]) => (
224
+ <span key={cls} className="class-pill">
225
+ <span className="w-3 h-3 rounded-full flex-shrink-0" style={{ backgroundColor: hex }} />
226
+ {cls}
227
+ </span>
228
+ ))}
229
+ </div>
230
+ </div>
231
+ </div>
232
+ )
233
+ }
frontend/src/app/processing/[id]/page.tsx ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useRef } from 'react'
4
+ import { useRouter, useParams } from 'next/navigation'
5
+
6
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8000'
7
+
8
+ const VOC_COLORS: Record<string, string> = {
9
+ aeroplane: '#87CEEB', bicycle: '#FFA500', bird: '#FFD700',
10
+ boat: '#00BFFF', bottle: '#9400D3', bus: '#FF1493',
11
+ car: '#DC143C', cat: '#FF8C00', chair: '#8B4513',
12
+ cow: '#FFFF00', diningtable: '#D2691E', dog: '#BA55D3',
13
+ horse: '#FF69B4', motorbike: '#00FF7F', person: '#FF4500',
14
+ 'potted plant': '#228B22', sheep: '#F0E68C', sofa: '#00CED1',
15
+ train: '#0000FF', 'tv/monitor': '#7FFFD4',
16
+ }
17
+
18
+ const STATUS_LABELS: Record<string, string> = {
19
+ queued: 'Queued',
20
+ processing: 'Segmenting frames …',
21
+ done: 'Complete!',
22
+ error: 'Error',
23
+ }
24
+
25
+ export default function ProcessingPage() {
26
+ const router = useRouter()
27
+ const params = useParams()
28
+ const jobId = params?.id as string
29
+
30
+ const [pct, setPct] = useState(0)
31
+ const [status, setStatus] = useState<string>('queued')
32
+ const [detected, setDetected] = useState<string[]>([])
33
+ const [error, setError] = useState<string | null>(null)
34
+ const [elapsed, setElapsed] = useState(0)
35
+ const wsRef = useRef<WebSocket | null>(null)
36
+ const timerRef = useRef<NodeJS.Timeout | null>(null)
37
+
38
+ useEffect(() => {
39
+ if (!jobId) return
40
+
41
+ // Start elapsed timer
42
+ const startTime = Date.now()
43
+ timerRef.current = setInterval(() => {
44
+ setElapsed(Math.floor((Date.now() - startTime) / 1000))
45
+ }, 1000)
46
+
47
+ // Open WebSocket
48
+ const wsUrl = `${API_BASE.replace('http', 'ws')}/ws/${jobId}`
49
+ const ws = new WebSocket(wsUrl)
50
+ wsRef.current = ws
51
+
52
+ ws.onmessage = (evt) => {
53
+ const data = JSON.parse(evt.data)
54
+ setStatus(data.status)
55
+ if (data.pct !== undefined) setPct(data.pct)
56
+ if (data.detected) setDetected(data.detected)
57
+ if (data.status === 'done') {
58
+ setPct(100)
59
+ clearInterval(timerRef.current!)
60
+ setTimeout(() => router.push(`/result/${jobId}`), 1200)
61
+ }
62
+ if (data.status === 'error') {
63
+ setError(data.error ?? 'Segmentation failed.')
64
+ clearInterval(timerRef.current!)
65
+ }
66
+ }
67
+
68
+ ws.onerror = () => {
69
+ // Fallback: poll via HTTP if WS fails
70
+ pollStatus()
71
+ }
72
+
73
+ return () => {
74
+ ws.close()
75
+ clearInterval(timerRef.current!)
76
+ }
77
+ }, [jobId])
78
+
79
+ const pollStatus = async () => {
80
+ const interval = setInterval(async () => {
81
+ try {
82
+ const res = await fetch(`${API_BASE}/api/status/${jobId}`)
83
+ const data = await res.json()
84
+ setStatus(data.status)
85
+ if (data.pct !== undefined) setPct(data.pct)
86
+ if (data.detected) setDetected(data.detected)
87
+ if (data.status === 'done') {
88
+ clearInterval(interval)
89
+ clearInterval(timerRef.current!)
90
+ setTimeout(() => router.push(`/result/${jobId}`), 1200)
91
+ }
92
+ if (data.status === 'error') {
93
+ setError(data.error)
94
+ clearInterval(interval)
95
+ }
96
+ } catch (e) {
97
+ // ignore transient errors
98
+ }
99
+ }, 1000)
100
+ }
101
+
102
+ const formatTime = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, '0')}`
103
+
104
+ return (
105
+ <div className="max-w-2xl mx-auto px-4 py-20">
106
+ <div className="glass rounded-2xl p-10 shadow-2xl animate-fade-in">
107
+
108
+ {/* Status header */}
109
+ <div className="text-center mb-10">
110
+ <div className={`w-20 h-20 rounded-2xl mx-auto mb-5 flex items-center justify-center
111
+ ${status === 'done' ? 'bg-green-500/15' : status === 'error' ? 'bg-red-500/15' : 'bg-brand-500/15'}`}>
112
+ {status === 'done' ? (
113
+ <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2.5">
114
+ <polyline points="20 6 9 17 4 12"/>
115
+ </svg>
116
+ ) : status === 'error' ? (
117
+ <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="2.5">
118
+ <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
119
+ </svg>
120
+ ) : (
121
+ <svg className="animate-spin" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="2">
122
+ <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
123
+ </svg>
124
+ )}
125
+ </div>
126
+
127
+ <h1 className="text-2xl font-bold text-white mb-1">
128
+ {STATUS_LABELS[status] ?? status}
129
+ </h1>
130
+ <p className="text-gray-400 text-sm">
131
+ Job ID: <code className="text-brand-400 font-mono">{jobId?.slice(0, 8)}…</code>
132
+ {status === 'processing' && (
133
+ <span className="ml-3 text-gray-500">⏱ {formatTime(elapsed)}</span>
134
+ )}
135
+ </p>
136
+ </div>
137
+
138
+ {/* Progress Bar */}
139
+ {status !== 'error' && (
140
+ <div className="mb-8">
141
+ <div className="flex justify-between text-sm font-medium mb-2.5">
142
+ <span className="text-gray-300">Progress</span>
143
+ <span className={`${pct >= 100 ? 'text-green-400' : 'text-brand-400'}`}>{pct.toFixed(1)}%</span>
144
+ </div>
145
+ <div className="progress-track h-3">
146
+ <div className="progress-fill h-full" style={{ width: `${pct}%` }} />
147
+ </div>
148
+ </div>
149
+ )}
150
+
151
+ {/* Error */}
152
+ {error && (
153
+ <div className="p-4 rounded-xl bg-red-500/10 border border-red-500/30 text-red-400 text-sm mb-6">
154
+ <strong>Error:</strong> {error}
155
+ </div>
156
+ )}
157
+
158
+ {/* Detected classes */}
159
+ {detected.length > 0 && (
160
+ <div>
161
+ <p className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3">
162
+ Detected Objects ({detected.length})
163
+ </p>
164
+ <div className="flex flex-wrap gap-2">
165
+ {detected.map((cls) => (
166
+ <span key={cls} className="class-pill">
167
+ <span
168
+ className="w-3 h-3 rounded-full flex-shrink-0"
169
+ style={{ backgroundColor: VOC_COLORS[cls] ?? '#888' }}
170
+ />
171
+ {cls}
172
+ </span>
173
+ ))}
174
+ </div>
175
+ </div>
176
+ )}
177
+
178
+ {/* Queue state placeholder */}
179
+ {status === 'queued' && (
180
+ <div className="flex items-center gap-3 p-4 rounded-xl bg-white/5 border border-white/10">
181
+ <div className="flex gap-1">
182
+ {[0, 1, 2].map((i) => (
183
+ <span
184
+ key={i}
185
+ className="w-2.5 h-2.5 bg-brand-400 rounded-full animate-bounce"
186
+ style={{ animationDelay: `${i * 0.15}s` }}
187
+ />
188
+ ))}
189
+ </div>
190
+ <p className="text-gray-400 text-sm">Waiting for a worker to pick up this job …</p>
191
+ </div>
192
+ )}
193
+
194
+ {/* Shimmer stats while processing */}
195
+ {status === 'processing' && (
196
+ <div className="mt-6 grid grid-cols-3 gap-3">
197
+ {['Frames Processed', 'Objects Found', 'Time Elapsed'].map((label, i) => (
198
+ <div key={label} className="stat-card text-center">
199
+ <p className="text-xs text-gray-500 mb-1">{label}</p>
200
+ <p className="font-bold text-white">
201
+ {i === 0 ? `${pct.toFixed(0)}%` : i === 1 ? detected.length : formatTime(elapsed)}
202
+ </p>
203
+ </div>
204
+ ))}
205
+ </div>
206
+ )}
207
+
208
+ {/* Back button */}
209
+ <a
210
+ href="/"
211
+ className="mt-8 flex items-center justify-center gap-2 text-sm text-gray-400 hover:text-white transition-colors"
212
+ >
213
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
214
+ <polyline points="15 18 9 12 15 6"/>
215
+ </svg>
216
+ Back to upload
217
+ </a>
218
+ </div>
219
+ </div>
220
+ )
221
+ }
frontend/src/app/result/[id]/page.tsx ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useRef } from 'react'
4
+ import { useParams } from 'next/navigation'
5
+
6
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8000'
7
+
8
+ const VOC_COLORS: Record<string, string> = {
9
+ aeroplane: '#87CEEB', bicycle: '#FFA500', bird: '#FFD700',
10
+ boat: '#00BFFF', bottle: '#9400D3', bus: '#FF1493',
11
+ car: '#DC143C', cat: '#FF8C00', chair: '#8B4513',
12
+ cow: '#FFFF00', diningtable: '#D2691E', dog: '#BA55D3',
13
+ horse: '#FF69B4', motorbike: '#00FF7F', person: '#FF4500',
14
+ 'potted plant': '#228B22', sheep: '#F0E68C', sofa: '#00CED1',
15
+ train: '#0000FF', 'tv/monitor': '#7FFFD4',
16
+ }
17
+
18
+ export default function ResultPage() {
19
+ const params = useParams()
20
+ const jobId = params?.id as string
21
+ const videoRef = useRef<HTMLVideoElement>(null)
22
+ const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading')
23
+ const [detected, setDetected] = useState<string[]>([])
24
+ const [isPlaying, setIsPlaying] = useState(false)
25
+ const [currentTime, setCurrentTime] = useState(0)
26
+ const [duration, setDuration] = useState(0)
27
+ const [volume, setVolume] = useState(1)
28
+
29
+ const videoUrl = `${API_BASE}/api/video/${jobId}`
30
+ const downloadUrl = videoUrl
31
+
32
+ useEffect(() => {
33
+ if (!jobId) return
34
+ fetch(`${API_BASE}/api/status/${jobId}`)
35
+ .then(r => r.json())
36
+ .then(data => {
37
+ if (data.status === 'done') {
38
+ setDetected(data.detected || [])
39
+ setStatus('ready')
40
+ } else if (data.status === 'error') {
41
+ setStatus('error')
42
+ }
43
+ })
44
+ .catch(() => setStatus('error'))
45
+ }, [jobId])
46
+
47
+ const togglePlay = () => {
48
+ const v = videoRef.current
49
+ if (!v) return
50
+ if (v.paused) { v.play(); setIsPlaying(true) }
51
+ else { v.pause(); setIsPlaying(false) }
52
+ }
53
+
54
+ const onTimeUpdate = () => {
55
+ if (videoRef.current) setCurrentTime(videoRef.current.currentTime)
56
+ }
57
+
58
+ const onLoadedMetadata = () => {
59
+ if (videoRef.current) setDuration(videoRef.current.duration)
60
+ }
61
+
62
+ const seek = (e: React.ChangeEvent<HTMLInputElement>) => {
63
+ const t = parseFloat(e.target.value)
64
+ if (videoRef.current) { videoRef.current.currentTime = t; setCurrentTime(t) }
65
+ }
66
+
67
+ const changeVolume = (e: React.ChangeEvent<HTMLInputElement>) => {
68
+ const v = parseFloat(e.target.value)
69
+ if (videoRef.current) videoRef.current.volume = v
70
+ setVolume(v)
71
+ }
72
+
73
+ const formatTime = (s: number) => {
74
+ const m = Math.floor(s / 60)
75
+ const sec = Math.floor(s % 60)
76
+ return `${m}:${String(sec).padStart(2, '0')}`
77
+ }
78
+
79
+ if (status === 'loading') {
80
+ return (
81
+ <div className="max-w-4xl mx-auto px-4 py-20 text-center animate-fade-in">
82
+ <div className="glass rounded-2xl p-16 shadow-2xl">
83
+ <div className="w-16 h-16 rounded-2xl bg-brand-500/15 flex items-center justify-center mx-auto mb-5">
84
+ <svg className="animate-spin" width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="#6366f1" strokeWidth="2">
85
+ <path d="M21 12a9 9 0 1 1-6.219-8.56"/>
86
+ </svg>
87
+ </div>
88
+ <p className="text-gray-300 text-lg">Loading your result …</p>
89
+ </div>
90
+ </div>
91
+ )
92
+ }
93
+
94
+ if (status === 'error') {
95
+ return (
96
+ <div className="max-w-4xl mx-auto px-4 py-20 text-center animate-fade-in">
97
+ <div className="glass rounded-2xl p-16 shadow-2xl">
98
+ <div className="w-16 h-16 rounded-2xl bg-red-500/15 flex items-center justify-center mx-auto mb-5">
99
+ <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="2">
100
+ <circle cx="12" cy="12" r="10"/>
101
+ <line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/>
102
+ </svg>
103
+ </div>
104
+ <p className="text-gray-300 text-lg mb-2">Result not available</p>
105
+ <p className="text-gray-500 text-sm mb-6">The job may have failed or the result has expired.</p>
106
+ <a href="/" className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-brand-600 hover:bg-brand-500 text-white font-medium text-sm transition-colors">
107
+ Try again
108
+ </a>
109
+ </div>
110
+ </div>
111
+ )
112
+ }
113
+
114
+ return (
115
+ <div className="max-w-5xl mx-auto px-4 py-12 animate-fade-in">
116
+
117
+ {/* Success Banner */}
118
+ <div className="flex items-center justify-between mb-8 flex-wrap gap-4">
119
+ <div>
120
+ <div className="flex items-center gap-2 mb-1">
121
+ <span className="w-2 h-2 rounded-full bg-green-400"></span>
122
+ <span className="text-xs font-semibold text-green-400 uppercase tracking-wider">Segmentation Complete</span>
123
+ </div>
124
+ <h1 className="text-3xl font-bold text-white">Your Segmented Video</h1>
125
+ <p className="text-gray-400 text-sm mt-1">
126
+ Job: <code className="font-mono text-brand-400">{jobId?.slice(0, 8)}…</code>
127
+ {detected.length > 0 && ` · ${detected.length} object class${detected.length > 1 ? 'es' : ''} detected`}
128
+ </p>
129
+ </div>
130
+ <a
131
+ href={downloadUrl}
132
+ download={`segmented_${jobId?.slice(0, 8)}.mp4`}
133
+ className="flex items-center gap-2 px-5 py-2.5 rounded-xl
134
+ bg-gradient-to-r from-brand-600 to-purple-600
135
+ hover:from-brand-500 hover:to-purple-500
136
+ text-white font-semibold text-sm transition-all
137
+ hover:shadow-lg hover:shadow-brand-500/25 hover:-translate-y-0.5"
138
+ >
139
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2">
140
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
141
+ <polyline points="7 10 12 15 17 10"/>
142
+ <line x1="12" y1="15" x2="12" y2="3"/>
143
+ </svg>
144
+ Download MP4
145
+ </a>
146
+ </div>
147
+
148
+ {/* Video Player */}
149
+ <div className="glass rounded-2xl overflow-hidden shadow-2xl mb-6">
150
+ {/* Labels */}
151
+ <div className="flex text-xs font-semibold text-gray-400 uppercase tracking-wider px-5 pt-4 pb-2 border-b border-white/5">
152
+ <span className="w-1/2 text-center">Original</span>
153
+ <span className="w-1/2 text-center text-brand-400">Segmented Overlay</span>
154
+ </div>
155
+
156
+ {/* Video */}
157
+ <div className="bg-black relative">
158
+ <video
159
+ ref={videoRef}
160
+ src={videoUrl}
161
+ className="w-full max-h-[480px] object-contain"
162
+ onTimeUpdate={onTimeUpdate}
163
+ onLoadedMetadata={onLoadedMetadata}
164
+ onEnded={() => setIsPlaying(false)}
165
+ />
166
+ </div>
167
+
168
+ {/* Custom Controls */}
169
+ <div className="px-5 py-4 bg-surface-card/60 backdrop-blur-sm space-y-3">
170
+ {/* Seek bar */}
171
+ <div className="flex items-center gap-3">
172
+ <span className="text-xs text-gray-400 font-mono w-10">{formatTime(currentTime)}</span>
173
+ <input
174
+ type="range" min={0} max={duration || 1} step={0.1} value={currentTime}
175
+ onChange={seek}
176
+ className="flex-1 h-1.5 bg-white/10 rounded-full appearance-none cursor-pointer accent-brand-500"
177
+ />
178
+ <span className="text-xs text-gray-400 font-mono w-10 text-right">{formatTime(duration)}</span>
179
+ </div>
180
+
181
+ {/* Buttons row */}
182
+ <div className="flex items-center gap-4">
183
+ <button
184
+ onClick={togglePlay}
185
+ className="w-10 h-10 rounded-xl bg-brand-500/15 hover:bg-brand-500/25 flex items-center justify-center transition-colors"
186
+ >
187
+ {isPlaying ? (
188
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="#6366f1">
189
+ <rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>
190
+ </svg>
191
+ ) : (
192
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="#6366f1">
193
+ <polygon points="5 3 19 12 5 21 5 3"/>
194
+ </svg>
195
+ )}
196
+ </button>
197
+
198
+ {/* Volume */}
199
+ <div className="flex items-center gap-2 flex-1">
200
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#9ca3af" strokeWidth="2">
201
+ <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
202
+ {volume > 0 && <path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>}
203
+ {volume > 0.5 && <path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>}
204
+ </svg>
205
+ <input
206
+ type="range" min={0} max={1} step={0.05} value={volume}
207
+ onChange={changeVolume}
208
+ className="w-20 h-1.5 bg-white/10 rounded-full appearance-none cursor-pointer accent-brand-500"
209
+ />
210
+ </div>
211
+
212
+ <span className="text-xs text-gray-500">
213
+ Side-by-side: Original | Segmented
214
+ </span>
215
+ </div>
216
+ </div>
217
+ </div>
218
+
219
+ {/* Detected Objects */}
220
+ {detected.length > 0 && (
221
+ <div className="glass rounded-2xl p-6 mb-6 shadow-xl">
222
+ <h2 className="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-4">
223
+ 🎯 Detected Object Classes ({detected.length})
224
+ </h2>
225
+ <div className="flex flex-wrap gap-2">
226
+ {detected.map((cls) => (
227
+ <span key={cls} className="class-pill text-sm px-3 py-1">
228
+ <span
229
+ className="w-3.5 h-3.5 rounded-full flex-shrink-0"
230
+ style={{ backgroundColor: VOC_COLORS[cls] ?? '#888' }}
231
+ />
232
+ {cls}
233
+ </span>
234
+ ))}
235
+ </div>
236
+ </div>
237
+ )}
238
+
239
+ {/* Action Buttons */}
240
+ <div className="flex flex-wrap gap-3">
241
+ <a
242
+ href="/"
243
+ className="flex items-center gap-2 px-5 py-2.5 rounded-xl border border-white/10 hover:border-white/20 hover:bg-white/5 text-gray-300 hover:text-white font-medium text-sm transition-all"
244
+ >
245
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
246
+ <polyline points="15 18 9 12 15 6"/>
247
+ </svg>
248
+ Segment Another Video
249
+ </a>
250
+ <a
251
+ href={downloadUrl}
252
+ download
253
+ className="flex items-center gap-2 px-5 py-2.5 rounded-xl border border-brand-500/30 hover:border-brand-500/60 hover:bg-brand-500/5 text-brand-400 font-medium text-sm transition-all"
254
+ >
255
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
256
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
257
+ <polyline points="7 10 12 15 17 10"/>
258
+ <line x1="12" y1="15" x2="12" y2="3"/>
259
+ </svg>
260
+ Download Result
261
+ </a>
262
+ </div>
263
+ </div>
264
+ )
265
+ }
frontend/tailwind.config.js ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: [
4
+ './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
5
+ './src/components/**/*.{js,ts,jsx,tsx,mdx}',
6
+ './src/app/**/*.{js,ts,jsx,tsx,mdx}',
7
+ ],
8
+ theme: {
9
+ extend: {
10
+ fontFamily: {
11
+ sans: ['Inter', 'system-ui', 'sans-serif'],
12
+ },
13
+ colors: {
14
+ brand: {
15
+ 50: '#f0f4ff',
16
+ 100: '#e0e9ff',
17
+ 200: '#c7d7fe',
18
+ 300: '#a5b9fc',
19
+ 400: '#8093f8',
20
+ 500: '#6366f1',
21
+ 600: '#4f46e5',
22
+ 700: '#4338ca',
23
+ 800: '#3730a3',
24
+ 900: '#312e81',
25
+ },
26
+ surface: {
27
+ DEFAULT: '#0f1117',
28
+ card: '#1a1d27',
29
+ border: '#2a2d3a',
30
+ },
31
+ },
32
+ animation: {
33
+ 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
34
+ 'fade-in': 'fadeIn 0.5s ease-out',
35
+ 'slide-up': 'slideUp 0.4s ease-out',
36
+ 'shimmer': 'shimmer 2s linear infinite',
37
+ },
38
+ keyframes: {
39
+ fadeIn: {
40
+ '0%': { opacity: '0' },
41
+ '100%': { opacity: '1' },
42
+ },
43
+ slideUp: {
44
+ '0%': { opacity: '0', transform: 'translateY(20px)' },
45
+ '100%': { opacity: '1', transform: 'translateY(0)' },
46
+ },
47
+ shimmer: {
48
+ '0%': { backgroundPosition: '-200% 0' },
49
+ '100%': { backgroundPosition: '200% 0' },
50
+ },
51
+ },
52
+ backgroundImage: {
53
+ 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
54
+ 'mesh-gradient': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
55
+ },
56
+ },
57
+ },
58
+ plugins: [],
59
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es5",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": { "@/*": ["./src/*"] }
18
+ },
19
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
20
+ "exclude": ["node_modules"]
21
+ }
start.sh ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # start.sh — Start the SegVision stack locally
3
+ # Run from: video-seg-app/ directory
4
+ # Usage: bash start.sh
5
+
6
+ set -e
7
+ CYAN='\033[0;36m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
8
+
9
+ PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
+ BACKEND_DIR="$PROJECT_DIR/backend"
11
+ FRONTEND_DIR="$PROJECT_DIR/frontend"
12
+ PIDS=()
13
+
14
+ # ── Detect Python / pip (Anaconda preferred) ────────────────────────────────
15
+ PYTHON=""
16
+ UVICORN=""
17
+ CELERY=""
18
+ for candidate in /opt/anaconda3/bin/python3 /opt/homebrew/bin/python3 python3 python; do
19
+ if command -v "$candidate" &>/dev/null 2>&1; then
20
+ PYTHON=$(command -v "$candidate")
21
+ break
22
+ fi
23
+ done
24
+ for candidate in /opt/anaconda3/bin/uvicorn /opt/homebrew/bin/uvicorn "$HOME/Library/Python/3.9/bin/uvicorn" uvicorn; do
25
+ if [ -x "$candidate" ]; then UVICORN="$candidate"; break; fi
26
+ done
27
+ for candidate in /opt/anaconda3/bin/celery /opt/homebrew/bin/celery "$HOME/Library/Python/3.9/bin/celery" celery; do
28
+ if [ -x "$candidate" ]; then CELERY="$candidate"; break; fi
29
+ done
30
+
31
+ echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
32
+ echo -e "${CYAN} SegVision — AI Video Segmentation${NC}"
33
+ echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
34
+ echo ""
35
+ echo -e " Python : ${GREEN}$PYTHON${NC}"
36
+ echo -e " Uvicorn: ${GREEN}${UVICORN:-NOT FOUND}${NC}"
37
+ echo -e " Celery : ${GREEN}${CELERY:-NOT FOUND}${NC}"
38
+ echo ""
39
+
40
+ if [ -z "$UVICORN" ]; then
41
+ echo -e "${RED}[ERROR]${NC} uvicorn not found. Run:"
42
+ echo -e " /opt/anaconda3/bin/pip install fastapi 'uvicorn[standard]' 'celery[redis]' redis python-multipart opencv-python-headless"
43
+ exit 1
44
+ fi
45
+
46
+ cleanup() {
47
+ echo -e "\n${YELLOW}Shutting down all services …${NC}"
48
+ for pid in "${PIDS[@]}"; do kill "$pid" 2>/dev/null || true; done
49
+ # Also clean up any stray processes
50
+ pkill -f "celery -A tasks" 2>/dev/null || true
51
+ pkill -f "uvicorn main:app" 2>/dev/null || true
52
+ exit 0
53
+ }
54
+ trap cleanup SIGINT SIGTERM
55
+
56
+ # ── Kill any stale processes from previous runs ───────────────────────────────
57
+ echo -e "${YELLOW}Stopping any existing services …${NC}"
58
+ pkill -f "celery -A tasks" 2>/dev/null && echo " Stopped old Celery worker" || true
59
+ pkill -f "uvicorn main:app" 2>/dev/null && echo " Stopped old Uvicorn server" || true
60
+ sleep 1
61
+
62
+ # ── 1. Redis ─────────────────────────────────────────────────────────────────
63
+ echo -e "${GREEN}[1/4]${NC} Checking Redis …"
64
+ if redis-cli ping &>/dev/null 2>&1; then
65
+ echo " ✓ Redis already running on :6379"
66
+ else
67
+ # Try docker (Desktop app path first, then PATH)
68
+ DOCKER_BIN=$(command -v docker 2>/dev/null || ls /Applications/Docker.app/Contents/Resources/bin/docker 2>/dev/null || echo "")
69
+ if [ -n "$DOCKER_BIN" ]; then
70
+ "$DOCKER_BIN" run -d --rm --name seg_redis -p 6379:6379 redis:7-alpine &>/dev/null || \
71
+ "$DOCKER_BIN" start seg_redis &>/dev/null || true
72
+ sleep 2
73
+ echo " ✓ Redis started via Docker"
74
+ else
75
+ echo -e " ${RED}[ERROR]${NC} Redis not found. Start Docker Desktop or run: brew install redis && redis-server"
76
+ exit 1
77
+ fi
78
+ fi
79
+
80
+ # ── 2. Celery Worker ─────────────────────────────────────────────────────────
81
+ echo -e "${GREEN}[2/4]${NC} Starting Celery worker (updated code) …"
82
+ cd "$BACKEND_DIR"
83
+ CELERY_LOG="/tmp/celery_worker.log"
84
+ "$CELERY" -A tasks worker --loglevel=info --concurrency=1 > "$CELERY_LOG" 2>&1 &
85
+ CELERY_PID=$!
86
+ PIDS+=($CELERY_PID)
87
+ sleep 2
88
+ if kill -0 $CELERY_PID 2>/dev/null; then
89
+ echo " ✓ Worker PID=$CELERY_PID (logs: $CELERY_LOG)"
90
+ else
91
+ echo -e " ${RED}[ERROR]${NC} Celery failed to start. Check: tail $CELERY_LOG"
92
+ exit 1
93
+ fi
94
+
95
+ # ── 3. FastAPI Server ─────────────────────────────────────────────────────────
96
+ echo -e "${GREEN}[3/4]${NC} Starting FastAPI on :8000 (with hot-reload) …"
97
+ "$UVICORN" main:app --host 0.0.0.0 --port 8000 --reload &
98
+ FASTAPI_PID=$!
99
+ PIDS+=($FASTAPI_PID)
100
+ echo " ✓ Backend PID=$FASTAPI_PID"
101
+ echo " ⚠️ Note: FastAPI hot-reloads on code save, but Celery does NOT."
102
+ echo " Restart this script after changing inference.py or tasks.py"
103
+
104
+ # ── 4. Frontend ─────────────────────────────────────────────────────────��─────
105
+ cd "$FRONTEND_DIR"
106
+ # Load nvm if available
107
+ export NVM_DIR="$HOME/.nvm"
108
+ [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
109
+ NPM_BIN=$(command -v npm 2>/dev/null || echo "")
110
+
111
+ if [ -n "$NPM_BIN" ]; then
112
+ echo -e "${GREEN}[4/4]${NC} Starting Next.js on :3000 …"
113
+ [ ! -d "node_modules" ] && "$NPM_BIN" install --silent
114
+ "$NPM_BIN" run dev &
115
+ PIDS+=($!)
116
+ echo " ✓ Frontend PID=$!"
117
+ FRONTEND_URL="http://localhost:3000"
118
+ else
119
+ echo -e "${YELLOW}[4/4]${NC} npm not found — skipping Next.js"
120
+ echo " Install Node.js: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash"
121
+ echo " Then: export NVM_DIR=~/.nvm && . \$NVM_DIR/nvm.sh && nvm install 20"
122
+ FRONTEND_URL="N/A (install Node.js)"
123
+ fi
124
+
125
+ echo ""
126
+ echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
127
+ echo -e " 🎬 Frontend : ${GREEN}${FRONTEND_URL}${NC}"
128
+ echo -e " ⚡ Backend : ${GREEN}http://localhost:8000${NC}"
129
+ echo -e " 📄 API Docs : ${GREEN}http://localhost:8000/docs${NC}"
130
+ echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
131
+ echo -e " Press ${YELLOW}Ctrl+C${NC} to stop all services"
132
+ echo ""
133
+
134
+ wait