arihant3704 commited on
Commit
ec0daf5
·
verified ·
1 Parent(s): 8bf4052

Upload 14 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ static/ui_preview.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use official Python 3.10 slim image
2
+ FROM python:3.10-slim
3
+
4
+ # Install system dependencies
5
+ RUN apt-get update && apt-get install -y \
6
+ ffmpeg \
7
+ libgl1-mesa-glx \
8
+ libglib2.0-0 \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Set working directory
12
+ WORKDIR /app
13
+
14
+ # Create a non-root user for Hugging Face Spaces
15
+ RUN useradd -m -u 1000 user
16
+ USER user
17
+ ENV PATH="/home/user/.local/bin:${PATH}"
18
+
19
+ # Copy requirements and install
20
+ COPY --chown=user:user requirements.txt .
21
+ RUN pip install --no-cache-dir --user -r requirements.txt
22
+
23
+ # Copy the rest of the application
24
+ COPY --chown=user:user . .
25
+
26
+ # Create uploads directory structure explicitly to ensure permissions
27
+ RUN mkdir -p uploads/models uploads/videos uploads/results uploads/temp
28
+
29
+ # Set environment variables
30
+ ENV PORT=7860
31
+ ENV PYTHONUNBUFFERED=1
32
+
33
+ # Expose the application port
34
+ EXPOSE 7860
35
+
36
+ # Command to run the application
37
+ CMD ["python", "main.py"]
README.md CHANGED
@@ -1,12 +1,63 @@
1
  ---
2
- title: Model Inference Studio
3
- emoji: 🏃
4
- colorFrom: green
5
- colorTo: blue
6
  sdk: docker
 
7
  pinned: false
8
- license: mit
9
- short_description: run your model with image and video online
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Inference Studio | AI Vision Explorer
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
 
 
9
  ---
10
 
11
+ # 🚀 Inference Studio | AI Vision Explorer
12
+
13
+ A premium, web-based interface for deploying and testing YOLO vision models with advanced controls for ROI (Region of Interest) filtering and confidence range analysis.
14
+
15
+ ![UI Preview](static/ui_preview.png)
16
+
17
+ ## ✨ Features
18
+
19
+ - **Interactive ROI Drawing**: Draw detection zones directly on a preview image or video frame.
20
+ - **Confidence Range Filtering**: Test model behavior by specifying both Min and Max confidence thresholds (e.g., visualize only low-confidence detections).
21
+ - **Video Studio**: Full support for video inference with automatic frame extraction for ROI setup and H.264 transcoding for web playback.
22
+ - **Glassmorphism UI**: Modern, dark-themed interface built for a premium developer experience.
23
+ - **Model Management**: Easily swap `.pt` models on the fly.
24
+
25
+ ## 🛠️ Setup
26
+
27
+ ### Prerequisites
28
+ - **Python 3.8+**
29
+ - **FFmpeg**: Required for video processing.
30
+ ```bash
31
+ # Ubuntu/Debian
32
+ sudo apt update && sudo apt install ffmpeg
33
+ ```
34
+
35
+ ### Installation
36
+ 1. Clone the repository:
37
+ ```bash
38
+ git clone https://github.com/your-repo/inference-studio.git
39
+ cd inference-studio
40
+ ```
41
+ 2. Install dependencies:
42
+ ```bash
43
+ pip install -r requirements.txt
44
+ ```
45
+
46
+ ## 🚀 Running the Studio
47
+
48
+ Start the server using the provided shell script:
49
+ ```bash
50
+ chmod +x start.sh
51
+ ./start.sh
52
+ ```
53
+ The studio will be available at `http://localhost:8000`.
54
+
55
+ ## 📂 Project Structure
56
+
57
+ - `main.py`: FastAPI backend handling inference and video tasks.
58
+ - `static/`: Frontend assets (styles, interactive JS).
59
+ - `templates/`: HTML templates.
60
+ - `uploads/`: Directory structure for models, videos, and results (ignored by Git).
61
+
62
+ ## 📝 License
63
+ MIT
__pycache__/main.cpython-310.pyc ADDED
Binary file (7.25 kB). View file
 
main.py ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ import shutil
4
+ import base64
5
+ import json
6
+ from typing import Optional
7
+ from fastapi import FastAPI, UploadFile, File, Request, HTTPException, Form
8
+ from fastapi.responses import HTMLResponse
9
+ from fastapi.staticfiles import StaticFiles
10
+ from fastapi.templating import Jinja2Templates
11
+ from ultralytics import YOLO
12
+ import cv2
13
+ import numpy as np
14
+ from pathlib import Path
15
+ import uuid
16
+ import time
17
+ from fastapi import BackgroundTasks
18
+ from fastapi.responses import FileResponse
19
+
20
+ app = FastAPI()
21
+
22
+ # Setup paths
23
+ BASE_DIR = Path(__file__).resolve().parent
24
+ UPLOAD_DIR = BASE_DIR / "uploads"
25
+ MODEL_DIR = UPLOAD_DIR / "models"
26
+ VIDEO_DIR = UPLOAD_DIR / "videos"
27
+ RESULT_DIR = UPLOAD_DIR / "results"
28
+ TEMP_DIR = UPLOAD_DIR / "temp"
29
+
30
+ for d in [MODEL_DIR, TEMP_DIR, VIDEO_DIR, RESULT_DIR]:
31
+ d.mkdir(parents=True, exist_ok=True)
32
+
33
+ # Global model state and task tracking
34
+ current_model = None
35
+ model_name = ""
36
+ video_tasks = {} # task_id: {"progress": P, "status": S, "result": R}
37
+
38
+ app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
39
+ templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
40
+
41
+ @app.get("/", response_class=HTMLResponse)
42
+ async def read_root(request: Request):
43
+ return templates.TemplateResponse("index.html", {
44
+ "request": request,
45
+ "model_loaded": current_model is not None,
46
+ "model_name": model_name
47
+ })
48
+
49
+ @app.post("/upload-model")
50
+ async def upload_model(file: UploadFile = File(...)):
51
+ global current_model, model_name
52
+ if not file.filename.endswith(".pt"):
53
+ raise HTTPException(status_code=400, detail="Only .pt files are supported")
54
+
55
+ file_path = MODEL_DIR / file.filename
56
+ with open(file_path, "wb") as buffer:
57
+ shutil.copyfileobj(file.file, buffer)
58
+
59
+ try:
60
+ current_model = YOLO(str(file_path))
61
+ model_name = file.filename
62
+ return {"status": "success", "message": f"Model {model_name} loaded successfully"}
63
+ except Exception as e:
64
+ if os.path.exists(file_path):
65
+ os.remove(file_path)
66
+ raise HTTPException(status_code=500, detail=f"Failed to load model: {str(e)}")
67
+
68
+ def apply_roi_filter(results, roi, img_w, img_h):
69
+ if not roi:
70
+ return results, []
71
+
72
+ x1_roi = int(roi['x1'] * img_w / 100)
73
+ y1_roi = int(roi['y1'] * img_h / 100)
74
+ x2_roi = int(roi['x2'] * img_w / 100)
75
+ y2_roi = int(roi['y2'] * img_h / 100)
76
+
77
+ indices = []
78
+ for i, box in enumerate(results.boxes):
79
+ bx1, by1, bx2, by2 = box.xyxy[0].tolist()
80
+ bcx = (bx1 + bx2) / 2
81
+ bcy = (by1 + by2) / 2
82
+
83
+ if x1_roi <= bcx <= x2_roi and y1_roi <= bcy <= y2_roi:
84
+ indices.append(i)
85
+
86
+ results.boxes = results.boxes[indices]
87
+ return results, [x1_roi, y1_roi, x2_roi, y2_roi]
88
+
89
+ def draw_roi_on_img(img, roi_coords):
90
+ if not roi_coords:
91
+ return img
92
+ x1, y1, x2, y2 = roi_coords
93
+ # Draw a dashed or semi-transparent rectangle for ROI
94
+ overlay = img.copy()
95
+ cv2.rectangle(overlay, (x1, y1), (x2, y2), (0, 255, 255), 2)
96
+ cv2.putText(overlay, "ROI ZONE", (x1 + 5, y1 + 25), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
97
+ return cv2.addWeighted(overlay, 0.6, img, 0.4, 0)
98
+
99
+ @app.post("/inference")
100
+ async def run_inference(
101
+ file: UploadFile = File(...),
102
+ conf_min: float = Form(0.25),
103
+ conf_max: float = Form(1.0),
104
+ roi: Optional[str] = Form(None)
105
+ ):
106
+ global current_model
107
+ if current_model is None:
108
+ raise HTTPException(status_code=400, detail="No model loaded. Please upload a model first.")
109
+
110
+ # Parse ROI if present
111
+ roi_data = json.loads(roi) if roi else None
112
+
113
+ # Read image
114
+ contents = await file.read()
115
+ nparr = np.frombuffer(contents, np.uint8)
116
+ img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
117
+
118
+ if img is None:
119
+ raise HTTPException(status_code=400, detail="Invalid image file")
120
+
121
+ h, w = img.shape[:2]
122
+
123
+ # Run inference with min threshold
124
+ results = current_model(img, conf=conf_min)[0]
125
+
126
+ # Apply max confidence filtering
127
+ if conf_max < 1.0:
128
+ indices = [i for i, box in enumerate(results.boxes) if float(box.conf[0]) <= conf_max]
129
+ results.boxes = results.boxes[indices]
130
+
131
+ # Apply ROI filtering
132
+ results, roi_coords = apply_roi_filter(results, roi_data, w, h)
133
+
134
+ # Draw results
135
+ annotated_img = results.plot()
136
+
137
+ # Draw ROI box
138
+ annotated_img = draw_roi_on_img(annotated_img, roi_coords)
139
+
140
+ # Encode to base64
141
+ _, buffer = cv2.imencode('.jpg', annotated_img)
142
+ img_str = base64.b64encode(buffer).decode('utf-8')
143
+
144
+ # Extract box info
145
+ boxes = []
146
+ for box in results.boxes:
147
+ boxes.append({
148
+ "cls": int(box.cls[0]),
149
+ "conf": float(box.conf[0]),
150
+ "xyxy": box.xyxy[0].tolist()
151
+ })
152
+
153
+ return {
154
+ "status": "success",
155
+ "image": f"data:image/jpeg;base64,{img_str}",
156
+ "count": len(results.boxes),
157
+ "boxes": boxes
158
+ }
159
+
160
+ def process_video_task(task_id: str, input_path: str, output_path: str, conf_min: float, conf_max: float, roi: Optional[dict]):
161
+ global current_model, video_tasks
162
+
163
+ # Temporary path for OpenCV output
164
+ temp_output = str(RESULT_DIR / f"temp_{task_id}.mp4")
165
+
166
+ try:
167
+ cap = cv2.VideoCapture(input_path)
168
+ if not cap.isOpened():
169
+ video_tasks[task_id]["status"] = "error"
170
+ video_tasks[task_id]["message"] = "Could not open video file"
171
+ return
172
+
173
+ fps = cap.get(cv2.CAP_PROP_FPS)
174
+ w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
175
+ h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
176
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
177
+
178
+ # Using mp4v for the intermediate file
179
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
180
+ out = cv2.VideoWriter(temp_output, fourcc, fps, (w, h))
181
+
182
+ frame_count = 0
183
+ while cap.isOpened():
184
+ ret, frame = cap.read()
185
+ if not ret:
186
+ break
187
+
188
+ # Inference with min threshold
189
+ results = current_model(frame, conf=conf_min)[0]
190
+
191
+ # Apply max confidence filtering
192
+ if conf_max < 1.0:
193
+ indices = [i for i, box in enumerate(results.boxes) if float(box.conf[0]) <= conf_max]
194
+ results.boxes = results.boxes[indices]
195
+
196
+ # Apply ROI filtering
197
+ results, roi_coords = apply_roi_filter(results, roi, w, h)
198
+
199
+ # Draw results
200
+ annotated_frame = results.plot()
201
+
202
+ # Draw ROI box
203
+ annotated_frame = draw_roi_on_img(annotated_frame, roi_coords)
204
+
205
+ out.write(annotated_frame)
206
+ frame_count += 1
207
+
208
+ # Update progress (0-90% for processing)
209
+ progress = int((frame_count / total_frames) * 90)
210
+ video_tasks[task_id]["progress"] = progress
211
+
212
+ cap.release()
213
+ out.release()
214
+
215
+ # Transcode to H.264 for web compatibility
216
+ video_tasks[task_id]["progress"] = 95
217
+ video_tasks[task_id]["status"] = "transcoding"
218
+
219
+ ffmpeg_cmd = [
220
+ 'ffmpeg', '-y', '-i', temp_output,
221
+ '-c:v', 'libx264', '-preset', 'ultrafast', '-crf', '28',
222
+ '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '128k',
223
+ output_path
224
+ ]
225
+
226
+ subprocess.run(ffmpeg_cmd, check=True, capture_output=True)
227
+
228
+ video_tasks[task_id]["progress"] = 100
229
+ video_tasks[task_id]["status"] = "completed"
230
+ video_tasks[task_id]["result_url"] = f"/video-result/{task_id}"
231
+
232
+ except Exception as e:
233
+ video_tasks[task_id]["status"] = "error"
234
+ video_tasks[task_id]["message"] = str(e)
235
+ finally:
236
+ # Cleanup files
237
+ if os.path.exists(input_path):
238
+ os.remove(input_path)
239
+ if os.path.exists(temp_output):
240
+ os.remove(temp_output)
241
+
242
+ @app.post("/inference-video")
243
+ async def run_video_inference(
244
+ background_tasks: BackgroundTasks,
245
+ file: UploadFile = File(...),
246
+ conf_min: float = Form(0.25),
247
+ conf_max: float = Form(1.0),
248
+ roi: Optional[str] = Form(None)
249
+ ):
250
+ global current_model, video_tasks
251
+ if current_model is None:
252
+ raise HTTPException(status_code=400, detail="No model loaded. Please upload a model first.")
253
+
254
+ # Parse ROI
255
+ roi_data = json.loads(roi) if roi else None
256
+
257
+ task_id = str(uuid.uuid4())
258
+ input_filename = f"{task_id}_{file.filename}"
259
+ input_path = VIDEO_DIR / input_filename
260
+ output_filename = f"processed_{task_id}.mp4"
261
+ output_path = RESULT_DIR / output_filename
262
+
263
+ with open(input_path, "wb") as buffer:
264
+ shutil.copyfileobj(file.file, buffer)
265
+
266
+ video_tasks[task_id] = {
267
+ "progress": 0,
268
+ "status": "processing",
269
+ "filename": file.filename
270
+ }
271
+
272
+ background_tasks.add_task(process_video_task, task_id, str(input_path), str(output_path), conf_min, conf_max, roi_data)
273
+
274
+ return {"status": "success", "task_id": task_id}
275
+
276
+ @app.get("/video-progress/{task_id}")
277
+ async def get_video_progress(task_id: str):
278
+ if task_id not in video_tasks:
279
+ raise HTTPException(status_code=404, detail="Task not found")
280
+ return video_tasks[task_id]
281
+
282
+ @app.get("/video-result/{task_id}")
283
+ async def get_video_result(task_id: str):
284
+ output_filename = f"processed_{task_id}.mp4"
285
+ output_path = RESULT_DIR / output_filename
286
+ if not output_path.exists():
287
+ raise HTTPException(status_code=404, detail="Result not found or still processing")
288
+ return FileResponse(path=output_path, filename=f"inference_{task_id}.mp4", media_type="video/mp4")
289
+
290
+ if __name__ == "__main__":
291
+ import uvicorn
292
+ # Use port from environment variable for Hugging Face compatibility (default 7860)
293
+ port = int(os.environ.get("PORT", 7860))
294
+ uvicorn.run(app, host="0.0.0.0", port=port)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ ultralytics
4
+ opencv-python
5
+ numpy
6
+ jinja2
7
+ python-multipart
start.sh ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Configuration
4
+ PORT=8000
5
+ HOST="0.0.0.0"
6
+
7
+ echo "------------------------------------------------"
8
+ echo "🚀 Starting Inference Studio..."
9
+ echo "------------------------------------------------"
10
+
11
+ # Check dependencies
12
+ echo "🔍 Checking dependencies..."
13
+
14
+ if ! command -v ffmpeg &> /dev/null; then
15
+ echo "⚠️ Warning: ffmpeg not found. Video transcoding will fail."
16
+ else
17
+ echo "✅ ffmpeg found."
18
+ fi
19
+
20
+ # Create necessary directories
21
+ echo "📁 Preparing directories..."
22
+ mkdir -p uploads/models uploads/videos uploads/results uploads/temp
23
+
24
+ # Start the server
25
+ echo "📡 Server starting at http://localhost:$PORT"
26
+ echo "------------------------------------------------"
27
+
28
+ # Run with python directly as main.py has the uvicorn runner
29
+ python3 main.py
static/app.js ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ // State management
3
+ let currentFile = null;
4
+ let isDrawing = false;
5
+ let startX, startY;
6
+ let roi = { x1: 0, y1: 0, x2: 100, y2: 100 };
7
+ let previewImage = new Image();
8
+
9
+ // Elements
10
+ const modelDropZone = document.getElementById('model-drop-zone');
11
+ const modelInput = document.getElementById('model-input');
12
+ const modelStatus = document.getElementById('model-status');
13
+ const statusText = document.getElementById('status-text');
14
+ const statusIcon = modelStatus.querySelector('i');
15
+
16
+ const mediaDropZone = document.getElementById('media-drop-zone');
17
+ const mediaInput = document.getElementById('media-input');
18
+
19
+ const previewSection = document.getElementById('preview-section');
20
+ const roiCanvas = document.getElementById('roi-canvas');
21
+ const ctx = roiCanvas.getContext('2d');
22
+
23
+ const thresholdInput = document.getElementById('threshold-input');
24
+ const confMaxInput = document.getElementById('conf-max-input');
25
+ const confRangeVal = document.getElementById('conf-range-val');
26
+ const roiX1 = document.getElementById('roi-x1');
27
+ const roiY1 = document.getElementById('roi-y1');
28
+ const roiX2 = document.getElementById('roi-x2');
29
+ const roiY2 = document.getElementById('roi-y2');
30
+ const resetRoiBtn = document.getElementById('reset-roi-btn');
31
+
32
+ const progressCard = document.getElementById('progress-card');
33
+ const loading = document.getElementById('loading');
34
+ const videoProgressContainer = document.getElementById('video-progress-container');
35
+ const videoProgressBar = document.getElementById('video-progress-bar');
36
+ const videoStatusMsg = document.getElementById('video-status-msg');
37
+ const videoPercentage = document.getElementById('video-percentage');
38
+
39
+ const analyzeBtn = document.getElementById('analyze-btn');
40
+
41
+ const resultSection = document.getElementById('result-section');
42
+ const resultImage = document.getElementById('result-image');
43
+ const resultCount = document.getElementById('result-count');
44
+ const downloadBtn = document.getElementById('download-btn');
45
+
46
+ const videoResultSection = document.getElementById('video-result-section');
47
+ const resultVideo = document.getElementById('result-video');
48
+ const videoDownloadBtn = document.getElementById('video-download-btn');
49
+
50
+ // Drag and Drop Setup
51
+ [modelDropZone, mediaDropZone].forEach(zone => {
52
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
53
+ zone.addEventListener(eventName, e => {
54
+ e.preventDefault();
55
+ e.stopPropagation();
56
+ });
57
+ });
58
+
59
+ ['dragenter', 'dragover'].forEach(eventName => {
60
+ zone.addEventListener(eventName, () => zone.classList.add('dragover'));
61
+ });
62
+
63
+ ['dragleave', 'drop'].forEach(eventName => {
64
+ zone.addEventListener(eventName, () => zone.classList.remove('dragover'));
65
+ });
66
+ });
67
+
68
+ // Clicks
69
+ modelDropZone.addEventListener('click', () => modelInput.click());
70
+ mediaDropZone.addEventListener('click', () => mediaInput.click());
71
+
72
+ // File Handlers
73
+ modelInput.addEventListener('change', e => handleModelUpload(e.target.files[0]));
74
+ modelDropZone.addEventListener('drop', e => handleModelUpload(e.dataTransfer.files[0]));
75
+
76
+ mediaInput.addEventListener('change', e => handleMediaSelection(e.target.files[0]));
77
+ mediaDropZone.addEventListener('drop', e => handleMediaSelection(e.dataTransfer.files[0]));
78
+
79
+ // Settings
80
+ const updateConfLabel = () => {
81
+ const min = Math.round(thresholdInput.value * 100);
82
+ const max = Math.round(confMaxInput.value * 100);
83
+ confRangeVal.innerText = `${min}% - ${max}%`;
84
+ };
85
+
86
+ thresholdInput.addEventListener('input', updateConfLabel);
87
+ confMaxInput.addEventListener('input', updateConfLabel);
88
+
89
+ [roiX1, roiY1, roiX2, roiY2].forEach(input => {
90
+ input.addEventListener('change', updateROIFromInputs);
91
+ });
92
+
93
+ resetRoiBtn.addEventListener('click', () => {
94
+ roi = { x1: 0, y1: 0, x2: 100, y2: 100 };
95
+ updateInputsFromROI();
96
+ drawROI();
97
+ });
98
+
99
+ analyzeBtn.addEventListener('click', startInference);
100
+
101
+ // --- Functions ---
102
+
103
+ async function handleModelUpload(file) {
104
+ if (!file || !file.name.endsWith('.pt')) {
105
+ showToast('Please upload a valid YOLO .pt model.', 'error');
106
+ return;
107
+ }
108
+
109
+ const formData = new FormData();
110
+ formData.append('file', file);
111
+
112
+ statusText.innerText = 'Uploading model...';
113
+ modelStatus.classList.remove('loaded');
114
+ statusIcon.className = 'fas fa-spinner fa-spin';
115
+
116
+ try {
117
+ const resp = await fetch('/upload-model', { method: 'POST', body: formData });
118
+ const data = await resp.json();
119
+ if (data.status === 'success') {
120
+ statusText.innerText = `Model: ${file.name}`;
121
+ modelStatus.classList.add('loaded');
122
+ statusIcon.className = 'fas fa-check-circle';
123
+ showToast('Model loaded successfully!', 'success');
124
+ } else {
125
+ throw new Error(data.detail);
126
+ }
127
+ } catch (err) {
128
+ statusText.innerText = 'Error loading model';
129
+ statusIcon.className = 'fas fa-exclamation-circle';
130
+ showToast(err.message, 'error');
131
+ }
132
+ }
133
+
134
+ async function handleMediaSelection(file) {
135
+ if (!file) return;
136
+ currentFile = file;
137
+
138
+ // Reset state
139
+ resultSection.classList.add('hidden');
140
+ videoResultSection.classList.add('hidden');
141
+ progressCard.classList.add('hidden');
142
+
143
+ if (file.type.startsWith('image/')) {
144
+ const reader = new FileReader();
145
+ reader.onload = e => {
146
+ previewImage.onload = () => initCanvas();
147
+ previewImage.src = e.target.result;
148
+ };
149
+ reader.readAsDataURL(file);
150
+ } else if (file.type.startsWith('video/')) {
151
+ extractVideoFrame(file);
152
+ } else {
153
+ showToast('Unsupported file type.', 'error');
154
+ }
155
+ }
156
+
157
+ function extractVideoFrame(file) {
158
+ const video = document.createElement('video');
159
+ video.preload = 'metadata';
160
+ video.src = URL.createObjectURL(file);
161
+ video.onloadedmetadata = () => {
162
+ video.currentTime = 0.1; // Seek a bit in to avoid black frames
163
+ };
164
+ video.onseeked = () => {
165
+ const tempCanvas = document.createElement('canvas');
166
+ tempCanvas.width = video.videoWidth;
167
+ tempCanvas.height = video.videoHeight;
168
+ const tempCtx = tempCanvas.getContext('2d');
169
+ tempCtx.drawImage(video, 0, 0);
170
+ previewImage.onload = () => initCanvas();
171
+ previewImage.src = tempCanvas.toDataURL('image/jpeg');
172
+ URL.revokeObjectURL(video.src);
173
+ };
174
+ }
175
+
176
+ function initCanvas() {
177
+ previewSection.classList.remove('hidden');
178
+ previewSection.scrollIntoView({ behavior: 'smooth' });
179
+
180
+ // Scale canvas to fit container but keep aspect ratio
181
+ const containerWidth = roiCanvas.parentElement.clientWidth;
182
+ const scale = containerWidth / previewImage.width;
183
+
184
+ roiCanvas.width = previewImage.width * scale;
185
+ roiCanvas.height = previewImage.height * scale;
186
+
187
+ drawROI();
188
+ }
189
+
190
+ function drawROI() {
191
+ ctx.clearRect(0, 0, roiCanvas.width, roiCanvas.height);
192
+ ctx.drawImage(previewImage, 0, 0, roiCanvas.width, roiCanvas.height);
193
+
194
+ // Darken outside
195
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';
196
+
197
+ const x1 = (roi.x1 / 100) * roiCanvas.width;
198
+ const y1 = (roi.y1 / 100) * roiCanvas.height;
199
+ const x2 = (roi.x2 / 100) * roiCanvas.width;
200
+ const y2 = (roi.y2 / 100) * roiCanvas.height;
201
+
202
+ const w = x2 - x1;
203
+ const h = y2 - y1;
204
+
205
+ // Draw overlay path with a "hole" for the ROI
206
+ ctx.beginPath();
207
+ ctx.rect(0, 0, roiCanvas.width, roiCanvas.height);
208
+ ctx.rect(x1, y1, w, h);
209
+ ctx.fill('evenodd');
210
+
211
+ // Draw border
212
+ ctx.strokeStyle = '#f59e0b';
213
+ ctx.lineWidth = 3;
214
+ ctx.setLineDash([5, 5]);
215
+ ctx.strokeRect(x1, y1, w, h);
216
+
217
+ // Optional: corner handles design look
218
+ ctx.fillStyle = '#f59e0b';
219
+ ctx.fillRect(x1-4, y1-4, 8, 8);
220
+ ctx.fillRect(x2-4, y1-4, 8, 8);
221
+ ctx.fillRect(x1-4, y2-4, 8, 8);
222
+ ctx.fillRect(x2-4, y2-4, 8, 8);
223
+ }
224
+
225
+ // Canvas Events
226
+ roiCanvas.addEventListener('mousedown', e => {
227
+ isDrawing = true;
228
+ const rect = roiCanvas.getBoundingClientRect();
229
+ startX = e.clientX - rect.left;
230
+ startY = e.clientY - rect.top;
231
+
232
+ roi.x1 = (startX / roiCanvas.width) * 100;
233
+ roi.y1 = (startY / roiCanvas.height) * 100;
234
+ });
235
+
236
+ roiCanvas.addEventListener('mousemove', e => {
237
+ if (!isDrawing) return;
238
+ const rect = roiCanvas.getBoundingClientRect();
239
+ const curX = e.clientX - rect.left;
240
+ const curY = e.clientY - rect.top;
241
+
242
+ roi.x2 = (curX / roiCanvas.width) * 100;
243
+ roi.y2 = (curY / roiCanvas.height) * 100;
244
+
245
+ updateInputsFromROI();
246
+ drawROI();
247
+ });
248
+
249
+ roiCanvas.addEventListener('mouseup', () => {
250
+ isDrawing = false;
251
+ // Normalize coordinates (ensure x1 < x2, y1 < y2)
252
+ if (roi.x1 > roi.x2) [roi.x1, roi.x2] = [roi.x2, roi.x1];
253
+ if (roi.y1 > roi.y2) [roi.y1, roi.y2] = [roi.y2, roi.y1];
254
+ updateInputsFromROI();
255
+ drawROI();
256
+ });
257
+
258
+ function updateInputsFromROI() {
259
+ roiX1.value = Math.round(roi.x1);
260
+ roiY1.value = Math.round(roi.y1);
261
+ roiX2.value = Math.round(roi.x2);
262
+ roiY2.value = Math.round(roi.y2);
263
+ }
264
+
265
+ function updateROIFromInputs() {
266
+ roi.x1 = parseInt(roiX1.value);
267
+ roi.y1 = parseInt(roiY1.value);
268
+ roi.x2 = parseInt(roiX2.value);
269
+ roi.y2 = parseInt(roiY2.value);
270
+ drawROI();
271
+ }
272
+
273
+ async function startInference() {
274
+ if (!currentFile) return;
275
+
276
+ progressCard.classList.remove('hidden');
277
+ progressCard.scrollIntoView({ behavior: 'smooth' });
278
+
279
+ const isVideo = currentFile.type.startsWith('video/');
280
+ const formData = new FormData();
281
+ formData.append('file', currentFile);
282
+ formData.append('conf_min', thresholdInput.value);
283
+ formData.append('conf_max', confMaxInput.value);
284
+ formData.append('roi', JSON.stringify(roi));
285
+
286
+ if (isVideo) {
287
+ handleVideoInference(formData);
288
+ } else {
289
+ handleImageInference(formData);
290
+ }
291
+ }
292
+
293
+ async function handleImageInference(formData) {
294
+ loading.classList.remove('hidden');
295
+ videoProgressContainer.classList.add('hidden');
296
+
297
+ try {
298
+ const resp = await fetch('/inference', { method: 'POST', body: formData });
299
+ const data = await resp.json();
300
+ if (data.status === 'success') {
301
+ resultImage.src = data.image;
302
+ resultCount.innerText = `${data.count} Detections`;
303
+ resultSection.classList.remove('hidden');
304
+ resultSection.scrollIntoView({ behavior: 'smooth' });
305
+ } else {
306
+ throw new Error(data.detail);
307
+ }
308
+ } catch (err) {
309
+ showToast(err.message, 'error');
310
+ } finally {
311
+ progressCard.classList.add('hidden');
312
+ }
313
+ }
314
+
315
+ async function handleVideoInference(formData) {
316
+ loading.classList.add('hidden');
317
+ videoProgressContainer.classList.remove('hidden');
318
+ videoProgressBar.style.width = '0%';
319
+ videoPercentage.innerText = '0%';
320
+ videoStatusMsg.innerText = 'Uploading video...';
321
+
322
+ try {
323
+ const resp = await fetch('/inference-video', { method: 'POST', body: formData });
324
+ const data = await resp.json();
325
+ if (data.status === 'success') {
326
+ videoStatusMsg.innerText = 'Processing frames...';
327
+ pollVideoProgress(data.task_id);
328
+ } else {
329
+ throw new Error(data.detail);
330
+ }
331
+ } catch (err) {
332
+ showToast(err.message, 'error');
333
+ progressCard.classList.add('hidden');
334
+ }
335
+ }
336
+
337
+ function pollVideoProgress(taskId) {
338
+ const interval = setInterval(async () => {
339
+ try {
340
+ const resp = await fetch(`/video-progress/${taskId}`);
341
+ const data = await resp.json();
342
+
343
+ if (data.status === 'processing') {
344
+ videoProgressBar.style.width = `${data.progress}%`;
345
+ videoPercentage.innerText = `${data.progress}%`;
346
+ } else if (data.status === 'completed') {
347
+ clearInterval(interval);
348
+ videoProgressBar.style.width = '100%';
349
+ videoPercentage.innerText = '100%';
350
+ videoStatusMsg.innerText = 'Processing complete!';
351
+
352
+ showVideoResult(taskId);
353
+ } else if (data.status === 'error') {
354
+ clearInterval(interval);
355
+ showToast(data.message, 'error');
356
+ progressCard.classList.add('hidden');
357
+ }
358
+ } catch (err) {
359
+ console.error(err);
360
+ }
361
+ }, 1000);
362
+ }
363
+
364
+ function showVideoResult(taskId) {
365
+ const url = `/video-result/${taskId}`;
366
+ resultVideo.src = url;
367
+ videoDownloadBtn.href = url;
368
+ videoResultSection.classList.remove('hidden');
369
+ videoResultSection.scrollIntoView({ behavior: 'smooth' });
370
+ progressCard.classList.add('hidden');
371
+ }
372
+
373
+ function showToast(message, type = 'info') {
374
+ const toast = document.createElement('div');
375
+ toast.className = `toast ${type}`;
376
+ Object.assign(toast.style, {
377
+ position: 'fixed', bottom: '20px', right: '20px', padding: '1rem 1.5rem',
378
+ borderRadius: '10px', color: 'white', zIndex: '1000',
379
+ background: type === 'error' ? '#ef4444' : '#10b981',
380
+ boxShadow: '0 4px 15px rgba(0,0,0,0.3)', animation: 'slideIn 0.3s ease forwards'
381
+ });
382
+ toast.innerText = message;
383
+ document.body.appendChild(toast);
384
+ setTimeout(() => {
385
+ toast.style.animation = 'slideOut 0.3s ease forwards';
386
+ setTimeout(() => toast.remove(), 300);
387
+ }, 3000);
388
+ }
389
+ });
static/style.css ADDED
@@ -0,0 +1,479 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary: #4f46e5;
3
+ --primary-hover: #4338ca;
4
+ --bg-dark: #0f172a;
5
+ --card-bg: rgba(30, 41, 59, 0.7);
6
+ --text-main: #f8fafc;
7
+ --text-muted: #94a3b8;
8
+ --accent: #06b6d4;
9
+ --success: #10b981;
10
+ --warning: #f59e0b;
11
+ --error: #ef4444;
12
+ }
13
+
14
+ * {
15
+ margin: 0;
16
+ padding: 0;
17
+ box-sizing: border-box;
18
+ font-family: 'Outfit', sans-serif;
19
+ }
20
+
21
+ body {
22
+ background-color: var(--bg-dark);
23
+ color: var(--text-main);
24
+ min-height: 100vh;
25
+ display: flex;
26
+ justify-content: center;
27
+ overflow-x: hidden;
28
+ }
29
+
30
+ .background-blob {
31
+ position: fixed;
32
+ width: 600px;
33
+ height: 600px;
34
+ background: radial-gradient(circle, rgba(79, 70, 229, 0.15) 0%, rgba(0,0,0,0) 70%);
35
+ top: -200px;
36
+ left: -200px;
37
+ z-index: -1;
38
+ animation: pulse 10s infinite alternate;
39
+ }
40
+
41
+ .blob-2 {
42
+ top: auto;
43
+ left: auto;
44
+ bottom: -200px;
45
+ right: -200px;
46
+ background: radial-gradient(circle, rgba(6, 182, 212, 0.15) 0%, rgba(0,0,0,0) 70%);
47
+ }
48
+
49
+ @keyframes pulse {
50
+ from { transform: scale(1); opacity: 0.5; }
51
+ to { transform: scale(1.2); opacity: 0.8; }
52
+ }
53
+
54
+ .container {
55
+ width: 100%;
56
+ max-width: 900px;
57
+ padding: 2rem;
58
+ position: relative;
59
+ z-index: 1;
60
+ }
61
+
62
+ header {
63
+ text-align: center;
64
+ margin-bottom: 3rem;
65
+ }
66
+
67
+ .logo {
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: center;
71
+ gap: 1rem;
72
+ margin-bottom: 0.5rem;
73
+ }
74
+
75
+ .logo i {
76
+ font-size: 2.5rem;
77
+ color: var(--accent);
78
+ filter: drop-shadow(0 0 10px rgba(6, 182, 212, 0.5));
79
+ }
80
+
81
+ .logo h1 {
82
+ font-size: 2.5rem;
83
+ font-weight: 700;
84
+ }
85
+
86
+ .logo span {
87
+ color: var(--primary);
88
+ }
89
+
90
+ .subtitle {
91
+ color: var(--text-muted);
92
+ font-size: 1.1rem;
93
+ }
94
+
95
+ .card {
96
+ background: var(--card-bg);
97
+ border-radius: 20px;
98
+ border: 1px solid rgba(255, 255, 255, 0.1);
99
+ padding: 1.5rem;
100
+ margin-bottom: 2rem;
101
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
102
+ }
103
+
104
+ .glass {
105
+ backdrop-filter: blur(12px);
106
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
107
+ }
108
+
109
+ .card:hover {
110
+ box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5);
111
+ border-color: rgba(255, 255, 255, 0.2);
112
+ }
113
+
114
+ .card-header {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: 1rem;
118
+ margin-bottom: 1.5rem;
119
+ }
120
+
121
+ .card-header i {
122
+ color: var(--accent);
123
+ font-size: 1.2rem;
124
+ }
125
+
126
+ .card-header h2 {
127
+ font-size: 1.3rem;
128
+ font-weight: 600;
129
+ }
130
+
131
+ .upload-zone {
132
+ border: 2px dashed rgba(255, 255, 255, 0.2);
133
+ border-radius: 15px;
134
+ padding: 2.5rem;
135
+ text-align: center;
136
+ cursor: pointer;
137
+ transition: all 0.3s ease;
138
+ }
139
+
140
+ .upload-zone:hover, .upload-zone.dragover {
141
+ background: rgba(255, 255, 255, 0.05);
142
+ border-color: var(--primary);
143
+ }
144
+
145
+ .upload-icon {
146
+ font-size: 2.5rem;
147
+ color: var(--text-muted);
148
+ margin-bottom: 1rem;
149
+ transition: color 0.3s ease;
150
+ }
151
+
152
+ .upload-zone:hover .upload-icon {
153
+ color: var(--primary);
154
+ }
155
+
156
+ .upload-zone span {
157
+ display: block;
158
+ margin-top: 0.5rem;
159
+ font-size: 0.9rem;
160
+ color: var(--text-muted);
161
+ }
162
+
163
+ .status-badge {
164
+ margin-top: 1rem;
165
+ padding: 0.6rem 1rem;
166
+ border-radius: 10px;
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 0.8rem;
170
+ background: rgba(239, 68, 68, 0.1);
171
+ color: var(--error);
172
+ font-size: 0.95rem;
173
+ }
174
+
175
+ .status-badge.loaded {
176
+ background: rgba(16, 185, 129, 0.1);
177
+ color: var(--success);
178
+ }
179
+
180
+ /* Redesigned Preview Area */
181
+ .preview-area {
182
+ width: 100%;
183
+ min-height: 300px;
184
+ background: radial-gradient(circle, #1e293b 0%, #000 100%);
185
+ border-radius: 12px;
186
+ overflow: hidden;
187
+ margin-bottom: 1.5rem;
188
+ display: flex;
189
+ align-items: center;
190
+ justify-content: center;
191
+ position: relative;
192
+ box-shadow: inset 0 0 40px rgba(0,0,0,0.9), 0 0 20px rgba(79, 70, 229, 0.1);
193
+ border: 1px solid rgba(255, 255, 255, 0.05);
194
+ }
195
+
196
+ #canvas-wrapper {
197
+ position: relative;
198
+ max-width: 100%;
199
+ }
200
+
201
+ #roi-canvas {
202
+ display: block;
203
+ max-width: 100%;
204
+ cursor: crosshair;
205
+ }
206
+
207
+ .hint-badge {
208
+ margin-left: auto;
209
+ font-size: 0.8rem;
210
+ background: var(--warning);
211
+ color: #000;
212
+ padding: 0.2rem 0.6rem;
213
+ border-radius: 20px;
214
+ font-weight: 600;
215
+ }
216
+
217
+ /* Settings Panel */
218
+ .settings-panel {
219
+ background: rgba(0,0,0,0.2);
220
+ padding: 1.2rem;
221
+ border-radius: 12px;
222
+ margin-bottom: 1.5rem;
223
+ display: grid;
224
+ grid-template-columns: 1fr 1fr;
225
+ gap: 1.5rem;
226
+ }
227
+
228
+ @media (max-width: 600px) {
229
+ .settings-panel { grid-template-columns: 1fr; }
230
+ }
231
+
232
+ .setting-item label {
233
+ display: block;
234
+ color: var(--text-muted);
235
+ font-size: 0.9rem;
236
+ margin-bottom: 0.8rem;
237
+ }
238
+
239
+ .setting-item.double-slider {
240
+ grid-column: 1 / -1;
241
+ background: rgba(255, 255, 255, 0.05);
242
+ padding: 1rem;
243
+ border-radius: 12px;
244
+ }
245
+
246
+ .slider-group {
247
+ display: flex;
248
+ flex-direction: column;
249
+ gap: 1rem;
250
+ }
251
+
252
+ .slider-row {
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 1rem;
256
+ }
257
+
258
+ .slider-label {
259
+ font-size: 0.8rem;
260
+ color: var(--text-muted);
261
+ min-width: 40px;
262
+ }
263
+
264
+ .roi-controls {
265
+ grid-column: 1 / -1;
266
+ display: flex;
267
+ flex-direction: column;
268
+ }
269
+
270
+ .label-with-toggle {
271
+ display: flex;
272
+ justify-content: space-between;
273
+ align-items: center;
274
+ margin-bottom: 0.8rem;
275
+ }
276
+
277
+ .btn-text {
278
+ background: none;
279
+ border: none;
280
+ color: var(--accent);
281
+ font-size: 0.85rem;
282
+ cursor: pointer;
283
+ padding: 0;
284
+ transition: opacity 0.2s;
285
+ }
286
+
287
+ .btn-text:hover { opacity: 0.8; }
288
+
289
+ .roi-inputs {
290
+ display: grid;
291
+ grid-template-columns: 1fr 1fr;
292
+ gap: 1.5rem;
293
+ }
294
+
295
+ .roi-group {
296
+ background: rgba(255, 255, 255, 0.03);
297
+ padding: 0.8rem;
298
+ border-radius: 10px;
299
+ border: 1px solid rgba(255, 255, 255, 0.05);
300
+ }
301
+
302
+ .group-label {
303
+ display: block;
304
+ font-size: 0.75rem;
305
+ color: var(--accent);
306
+ margin-bottom: 0.5rem;
307
+ font-weight: 600;
308
+ text-transform: uppercase;
309
+ letter-spacing: 0.05rem;
310
+ }
311
+
312
+ .coord-inputs {
313
+ display: flex;
314
+ gap: 0.8rem;
315
+ }
316
+
317
+ .coord-input {
318
+ flex: 1;
319
+ display: flex;
320
+ flex-direction: column;
321
+ gap: 0.3rem;
322
+ }
323
+
324
+ .coord-input span {
325
+ font-size: 0.65rem;
326
+ color: var(--text-muted);
327
+ }
328
+
329
+ .coord-input input {
330
+ width: 100%;
331
+ background: rgba(0, 0, 0, 0.3);
332
+ border: 1px solid rgba(255, 255, 255, 0.1);
333
+ color: white;
334
+ padding: 0.5rem;
335
+ border-radius: 8px;
336
+ text-align: center;
337
+ font-size: 0.95rem;
338
+ transition: border-color 0.2s;
339
+ }
340
+
341
+ .coord-input input:focus {
342
+ border-color: var(--primary);
343
+ outline: none;
344
+ }
345
+
346
+ /* Enhancing inputs */
347
+ input[type="range"] {
348
+ width: 100%;
349
+ height: 6px;
350
+ background: rgba(255, 255, 255, 0.1);
351
+ border-radius: 5px;
352
+ appearance: none;
353
+ outline: none;
354
+ }
355
+
356
+ input[type="range"]::-webkit-slider-thumb {
357
+ appearance: none;
358
+ width: 18px;
359
+ height: 18px;
360
+ background: var(--primary);
361
+ border: 2px solid var(--accent);
362
+ border-radius: 50%;
363
+ cursor: pointer;
364
+ box-shadow: 0 0 15px rgba(6, 182, 212, 0.4);
365
+ }
366
+
367
+ /* Actions */
368
+ .action-bar {
369
+ display: flex;
370
+ gap: 1rem;
371
+ justify-content: center;
372
+ }
373
+
374
+ .btn-primary {
375
+ background: var(--primary);
376
+ color: white;
377
+ border: none;
378
+ padding: 0.8rem 2rem;
379
+ border-radius: 10px;
380
+ font-weight: 600;
381
+ cursor: pointer;
382
+ display: flex;
383
+ align-items: center;
384
+ gap: 0.8rem;
385
+ transition: all 0.2s ease;
386
+ text-decoration: none;
387
+ font-size: 1rem;
388
+ }
389
+
390
+ .btn-primary:hover {
391
+ background: var(--primary-hover);
392
+ transform: translateY(-2px);
393
+ box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
394
+ }
395
+
396
+ .main-action {
397
+ width: 100%;
398
+ justify-content: center;
399
+ font-size: 1.1rem;
400
+ padding: 1rem;
401
+ }
402
+
403
+ /* Spinner & Progress */
404
+ .spinner-container {
405
+ text-align: center;
406
+ padding: 2rem;
407
+ }
408
+
409
+ .spinner {
410
+ width: 40px;
411
+ height: 40px;
412
+ border: 4px solid rgba(255, 255, 255, 0.1);
413
+ border-top: 4px solid var(--accent);
414
+ border-radius: 50%;
415
+ animation: spin 1s linear infinite;
416
+ margin: 0 auto 1rem;
417
+ }
418
+
419
+ @keyframes spin {
420
+ 0% { transform: rotate(0deg); }
421
+ 100% { transform: rotate(360deg); }
422
+ }
423
+
424
+ .progress-info {
425
+ display: flex;
426
+ justify-content: space-between;
427
+ margin-bottom: 0.8rem;
428
+ font-size: 0.95rem;
429
+ }
430
+
431
+ .progress-bar-bg {
432
+ background: rgba(255, 255, 255, 0.1);
433
+ height: 10px;
434
+ border-radius: 5px;
435
+ overflow: hidden;
436
+ }
437
+
438
+ .progress-bar-fill {
439
+ height: 100%;
440
+ background: linear-gradient(90deg, var(--primary), var(--accent));
441
+ width: 0%;
442
+ transition: width 0.3s ease;
443
+ }
444
+
445
+ /* Results */
446
+ .result-viewer {
447
+ display: flex;
448
+ flex-direction: column;
449
+ gap: 1.5rem;
450
+ }
451
+
452
+ .result-viewer img, .result-viewer video {
453
+ width: 100%;
454
+ border-radius: 12px;
455
+ border: 1px solid rgba(255, 255, 255, 0.1);
456
+ }
457
+
458
+ .badge {
459
+ margin-left: auto;
460
+ background: var(--success);
461
+ color: white;
462
+ padding: 0.25rem 0.8rem;
463
+ border-radius: 20px;
464
+ font-size: 0.85rem;
465
+ font-weight: 600;
466
+ }
467
+
468
+ .hidden { display: none !important; }
469
+
470
+ /* Toast */
471
+ @keyframes slideIn {
472
+ from { transform: translateX(100%); opacity: 0; }
473
+ to { transform: translateX(0); opacity: 1; }
474
+ }
475
+
476
+ @keyframes slideOut {
477
+ from { transform: translateX(0); opacity: 1; }
478
+ to { transform: translateX(100%); opacity: 0; }
479
+ }
static/ui_preview.png ADDED

Git LFS Details

  • SHA256: f4a2a49d6121ae0699fdda62be09e78fb28423cc82e69be6e9c25570f269b655
  • Pointer size: 131 Bytes
  • Size of remote file: 141 kB
templates/index.html ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Inference Studio | AI Vision Explorer</title>
7
+ <link rel="stylesheet" href="/static/style.css">
8
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap" rel="stylesheet">
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
10
+ </head>
11
+ <body>
12
+ <div class="background-blob"></div>
13
+ <div class="background-blob blob-2"></div>
14
+
15
+ <div class="container">
16
+ <header>
17
+ <div class="logo">
18
+ <i class="fas fa-brain"></i>
19
+ <h1>Inference<span>Studio</span></h1>
20
+ </div>
21
+ <p class="subtitle">Deploy and test your vision models in seconds</p>
22
+ </header>
23
+
24
+ <main>
25
+ <!-- Model Section -->
26
+ <section class="card glass" id="model-section">
27
+ <div class="card-header">
28
+ <i class="fas fa-microchip"></i>
29
+ <h2>Model Management</h2>
30
+ </div>
31
+ <input type="file" id="model-input" accept=".pt" hidden>
32
+ <div class="upload-zone" id="model-drop-zone">
33
+ <div class="upload-icon">
34
+ <i class="fas fa-cloud-upload-alt"></i>
35
+ </div>
36
+ <p>Drag & drop your <strong>YOLO .pt</strong> model</p>
37
+ <span>or click to browse files</span>
38
+ </div>
39
+ <div id="model-status" class="status-badge {% if model_loaded %}loaded{% endif %}">
40
+ <i class="fas {% if model_loaded %}fa-check-circle{% else %}fa-exclamation-circle{% endif %}"></i>
41
+ <span id="status-text">{% if model_loaded %}Model: {{ model_name }}{% else %}No model loaded{% endif %}</span>
42
+ </div>
43
+ </section>
44
+
45
+ <!-- Media Upload Section -->
46
+ <section class="card glass" id="upload-section">
47
+ <div class="card-header">
48
+ <i class="fas fa-file-import"></i>
49
+ <h2>Step 1: Upload Media</h2>
50
+ </div>
51
+ <input type="file" id="media-input" accept="image/*,video/*" hidden>
52
+ <div class="upload-zone" id="media-drop-zone">
53
+ <div class="upload-icon">
54
+ <i class="fas fa-photo-video"></i>
55
+ </div>
56
+ <p>Drag & drop <strong>Image</strong> or <strong>Video</strong></p>
57
+ <span>JPG, PNG, MP4, AVI, MOV supported</span>
58
+ </div>
59
+ </section>
60
+
61
+ <!-- Preview & ROI Section -->
62
+ <section class="card glass hidden" id="preview-section">
63
+ <div class="card-header">
64
+ <i class="fas fa-crosshairs"></i>
65
+ <h2>Step 2: Configure & Draw ROI</h2>
66
+ <span class="hint-badge">Click & Drag on Preview</span>
67
+ </div>
68
+
69
+ <div class="preview-area">
70
+ <div id="canvas-wrapper">
71
+ <canvas id="roi-canvas"></canvas>
72
+ </div>
73
+ </div>
74
+
75
+ <div class="settings-panel">
76
+ <div class="setting-item double-slider">
77
+ <label>Confidence Range: <span id="conf-range-val">25% - 100%</span></label>
78
+ <div class="slider-group">
79
+ <div class="slider-row">
80
+ <span class="slider-label">Min:</span>
81
+ <input type="range" id="threshold-input" min="0.01" max="1.0" step="0.01" value="0.25">
82
+ </div>
83
+ <div class="slider-row">
84
+ <span class="slider-label">Max:</span>
85
+ <input type="range" id="conf-max-input" min="0.01" max="1.0" step="0.01" value="1.0">
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <div class="roi-controls">
91
+ <div class="label-with-toggle">
92
+ <label>ROI Boundary (%)</label>
93
+ <button id="reset-roi-btn" class="btn-text">
94
+ <i class="fas fa-undo"></i> Reset
95
+ </button>
96
+ </div>
97
+ <div class="roi-inputs">
98
+ <div class="roi-group">
99
+ <span class="group-label">Top-Left</span>
100
+ <div class="coord-inputs">
101
+ <div class="coord-input">
102
+ <span>X1</span>
103
+ <input type="number" id="roi-x1" value="0">
104
+ </div>
105
+ <div class="coord-input">
106
+ <span>Y1</span>
107
+ <input type="number" id="roi-y1" value="0">
108
+ </div>
109
+ </div>
110
+ </div>
111
+ <div class="roi-group">
112
+ <span class="group-label">Bottom-Right</span>
113
+ <div class="coord-inputs">
114
+ <div class="coord-input">
115
+ <span>X2</span>
116
+ <input type="number" id="roi-x2" value="100">
117
+ </div>
118
+ <div class="coord-input">
119
+ <span>Y2</span>
120
+ <input type="number" id="roi-y2" value="100">
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <div class="action-bar">
129
+ <button id="analyze-btn" class="btn-primary main-action">
130
+ <i class="fas fa-play"></i> Start Inference
131
+ </button>
132
+ </div>
133
+ </section>
134
+
135
+ <!-- Progress Card -->
136
+ <section class="card glass hidden" id="progress-card">
137
+ <div id="loading" class="spinner-container">
138
+ <div class="spinner"></div>
139
+ <p>Running Vision AI Inference...</p>
140
+ </div>
141
+
142
+ <div id="video-progress-container" class="hidden">
143
+ <div class="progress-info">
144
+ <span id="video-status-msg">Processing video...</span>
145
+ <span id="video-percentage">0%</span>
146
+ </div>
147
+ <div class="progress-bar-bg">
148
+ <div id="video-progress-bar" class="progress-bar-fill"></div>
149
+ </div>
150
+ </div>
151
+ </section>
152
+
153
+ <!-- Results Section -->
154
+ <section class="card glass result-card hidden" id="video-result-section">
155
+ <div class="card-header">
156
+ <i class="fas fa-video"></i>
157
+ <h2>Video Results</h2>
158
+ </div>
159
+ <div class="result-viewer">
160
+ <video id="result-video" controls></video>
161
+ <div class="action-bar">
162
+ <a id="video-download-btn" class="btn-primary" download>
163
+ <i class="fas fa-download"></i> Download Video
164
+ </a>
165
+ </div>
166
+ </div>
167
+ </section>
168
+
169
+ <section class="card glass result-card hidden" id="result-section">
170
+ <div class="card-header">
171
+ <i class="fas fa-poll"></i>
172
+ <h2>Detection Summary</h2>
173
+ <span id="result-count" class="badge">0 Detections</span>
174
+ </div>
175
+ <div class="result-viewer">
176
+ <img id="result-image" src="" alt="Results">
177
+ <div class="action-bar">
178
+ <button id="download-btn" class="btn-primary">
179
+ <i class="fas fa-download"></i> Save Image
180
+ </button>
181
+ </div>
182
+ </div>
183
+ </section>
184
+ </main>
185
+ </div>
186
+
187
+ <script src="/static/app.js"></script>
188
+ </body>
189
+ </html>
uploads/models/.gitkeep ADDED
File without changes
uploads/results/.gitkeep ADDED
File without changes
uploads/temp/.gitkeep ADDED
File without changes
uploads/videos/.gitkeep ADDED
File without changes