LiamKhoaLe commited on
Commit
57328cd
·
1 Parent(s): 2e43ca6

Add YOLOv8n to detect animal and 2 Roboflow models to be specifically finetuned on bird and fish. Rm HTML_CONTENT to be rendered on statics dir

Browse files
.DS_Store CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
 
Dockerfile CHANGED
@@ -54,6 +54,7 @@ COPY --chown=user . $HOME/app
54
  RUN python -c "from huggingface_hub import snapshot_download; snapshot_download(repo_id='facebook/detr-resnet-50', local_dir='/home/user/app/model/detr', local_dir_use_symlinks=False)"
55
  RUN wget -O $HOME/app/model/garbage_detector.pt https://huggingface.co/BinKhoaLe1812/Garbage_Detection/resolve/main/garbage_detector.pt
56
  RUN wget -O $HOME/app/model/yolov5-detect-trash-classification.pt https://huggingface.co/turhancan97/yolov5-detect-trash-classification/resolve/main/yolov5s.pt
 
57
 
58
  # Verify model setup
59
  RUN python setup.py
 
54
  RUN python -c "from huggingface_hub import snapshot_download; snapshot_download(repo_id='facebook/detr-resnet-50', local_dir='/home/user/app/model/detr', local_dir_use_symlinks=False)"
55
  RUN wget -O $HOME/app/model/garbage_detector.pt https://huggingface.co/BinKhoaLe1812/Garbage_Detection/resolve/main/garbage_detector.pt
56
  RUN wget -O $HOME/app/model/yolov5-detect-trash-classification.pt https://huggingface.co/turhancan97/yolov5-detect-trash-classification/resolve/main/yolov5s.pt
57
+ RUN wget -O /home/user/app/model/yolov8n.pt https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n.pt
58
 
59
  # Verify model setup
60
  RUN python setup.py
app.py CHANGED
@@ -2,7 +2,7 @@
2
 
3
  # ───────────────────────── app.py (Sall-e demo) ─────────────────────────
4
  # FastAPI ▸ upload image ▸ multi-model garbage detection ▸ ADE-20K
5
- # semantic segmentation (Water / Garbage) ▸ A* + KNN navigation ▸ H.264 video
6
  # =======================================================================
7
 
8
  import os, uuid, threading, shutil, time, heapq, cv2, numpy as np
@@ -12,6 +12,7 @@ from fastapi import FastAPI, File, UploadFile, Request
12
  from fastapi.responses import HTMLResponse, StreamingResponse, Response
13
  from fastapi.staticfiles import StaticFiles
14
 
 
15
  # ── Vision libs ─────────────────────────────────────────────────────────
16
  import torch, yolov5, ffmpeg
17
  from ultralytics import YOLO
@@ -19,7 +20,9 @@ from transformers import (
19
  DetrImageProcessor, DetrForObjectDetection,
20
  SegformerFeatureExtractor, SegformerForSemanticSegmentation
21
  )
22
- from sklearn.neighbors import NearestNeighbors
 
 
23
 
24
  # ── Folders / files ─────────────────────────────────────────────────────
25
  BASE = "/home/user/app"
@@ -45,6 +48,7 @@ feat_extractor = SegformerFeatureExtractor.from_pretrained(
45
  "nvidia/segformer-b4-finetuned-ade-512-512")
46
  segformer = SegformerForSemanticSegmentation.from_pretrained(
47
  "nvidia/segformer-b4-finetuned-ade-512-512")
 
48
  print("✅ Models ready\n")
49
 
50
  # ── ADE-20K palette + custom mapping (verbatim) ─────────────────────────
@@ -163,6 +167,7 @@ def highlight_water_mask_on_frame(frame, binary_mask, color=(255, 0, 0), alpha=0
163
  cv2.drawContours(overlay, contours, -1, color, thickness=cv2.FILLED)
164
  return cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0)
165
 
 
166
  # ── A* and KNN over binary water grid ─────────────────────────────────
167
  def astar(start, goal, occ):
168
  h = lambda a,b: abs(a[0]-b[0])+abs(a[1]-b[1])
@@ -222,6 +227,7 @@ def knn_path(start, targets, occ):
222
  todo.remove(list(best))
223
  return path
224
 
 
225
  # ── Robot sprite/class -──────────────────────────────────────────────────
226
  class Robot:
227
  def __init__(self, sprite, speed=2000): # Declare the robot's physical stats and routing (position, speed, movement, path)
@@ -251,156 +257,29 @@ class Robot:
251
  break
252
 
253
 
254
- # ── FastAPI & HTML content (original styling) ───────────────────────────
255
- # HTML Content for UI (streamed with FastAPI HTML renderer)
256
- HTML_CONTENT = """
257
- <!DOCTYPE html>
258
- <html>
259
- <head>
260
- <title>Sall-e Garbage Detection</title>
261
- <link rel="website icon" type="png" href="/static/icon.png" >
262
- <style>
263
- body {
264
- font-family: 'Roboto', sans-serif; background: linear-gradient(270deg, rgb(44, 13, 58), rgb(13, 58, 56)); color: white; text-align: center; margin: 0; padding: 50px;
265
- }
266
- h1 {
267
- font-size: 40px;
268
- background: linear-gradient(to right, #f32170, #ff6b08, #cf23cf, #eedd44);
269
- -webkit-text-fill-color: transparent;
270
- -webkit-background-clip: text;
271
- font-weight: bold;
272
- }
273
- #upload-container {
274
- background: rgba(255, 255, 255, 0.2); padding: 20px; width: 70%; border-radius: 10px; display: inline-block; box-shadow: 0px 0px 10px rgba(255, 255, 255, 0.3);
275
- }
276
- #upload {
277
- font-size: 18px; padding: 10px; border-radius: 5px; border: none; background: #fff; cursor: pointer;
278
- }
279
- #loader {
280
- margin-top: 10px; margin-left: auto; margin-right: auto; width: 60px; height: 60px; font-size: 12px; text-align: center;
281
- }
282
- p {
283
- margin-top: 10px; font-size: 12px; color: #3498db;
284
- }
285
- #spinner {
286
- border: 8px solid #f3f3f3; border-top: 8px solid rgb(117 7 7); border-radius: 50%; animation: spin 1s linear infinite; width: 40px; height: 40px; margin: auto;
287
- }
288
- @keyframes spin {
289
- 0% { transform: rotate(0deg); }
290
- 100% { transform: rotate(360deg); }
291
- }
292
- #outputVideo {
293
- margin-top: 20px; width: 70%; margin-left: auto; margin-right: auto; max-width: 640px; border-radius: 10px; box-shadow: 0px 0px 10px rgba(255, 255, 255, 0.3);
294
- }
295
- #downloadBtn {
296
- display: block; visibility: hidden; width: 20%; margin-top: 20px; margin-left: auto; margin-right: auto; padding: 10px 15px; font-size: 16px; background: #27ae60; color: white; border: none; border-radius: 5px; cursor: pointer; text-decoration: none;
297
- }
298
- #downloadBtn:hover {
299
- background: #950606;
300
- }
301
- .hidden {
302
- display: none;
303
- }
304
- @media (max-width: 860px) {
305
- h1 { font-size: 30px; }
306
- }
307
- @media (max-width: 720px) {
308
- h1 { font-size: 25px; }
309
- #upload { font-size: 15px; }
310
- #downloadBtn { font-size: 13px; }
311
- }
312
- @media (max-width: 580px) {
313
- h1 { font-size: 20px; }
314
- #upload { font-size: 10px; }
315
- #downloadBtn { font-size: 10px; }
316
- }
317
- @media (max-width: 580px) {
318
- h1 { font-size: 10px; }
319
- }
320
- @media (max-width: 460px) {
321
- #upload { font-size: 7px; }
322
- }
323
- @media (max-width: 400px) {
324
- h1 { font-size: 14px; }
325
- }
326
- @media (max-width: 370px) {
327
- h1 { font-size: 11px; }
328
- #upload { font-size: 5px; }
329
- #downloadBtn { font-size: 7px; }
330
- }
331
- @media (max-width: 330px) {
332
- h1 { font-size: 8px; }
333
- #upload { font-size: 3px; }
334
- #downloadBtn { font-size: 5px; }
335
- }
336
- </style>
337
- </head>
338
- <body>
339
- <h1>Upload an Image for Garbage Detection</h1>
340
- <div id="upload-container">
341
- <input type="file" id="upload" accept="image/*">
342
- </div>
343
- <div id="loader" class="loader hidden">
344
- <div id="spinner"></div>
345
- <!-- <p>Garbage detection model processing...</p> -->
346
- </div>
347
- <video id="outputVideo" class="outputVideo" controls></video>
348
- <a id="downloadBtn" class="downloadBtn">Download Video</a>
349
- <script>
350
- document.addEventListener("DOMContentLoaded", function() {
351
- document.getElementById("outputVideo").classList.add("hidden");
352
- document.getElementById("downloadBtn").style.visibility = "hidden";
353
- });
354
- document.getElementById('upload').addEventListener('change', async function(event) {
355
- event.preventDefault();
356
- const loader = document.getElementById("loader");
357
- const outputVideo = document.getElementById("outputVideo");
358
- const downloadBtn = document.getElementById("downloadBtn");
359
- let file = event.target.files[0];
360
- if (file) {
361
- let formData = new FormData();
362
- formData.append("file", file);
363
- loader.classList.remove("hidden");
364
- outputVideo.classList.add("hidden");
365
- document.getElementById("downloadBtn").style.visibility = "hidden";
366
- let response = await fetch('/upload/', { method: 'POST', body: formData });
367
- let result = await response.json();
368
- let user_id = result.user_id;
369
- while (true) {
370
- let checkResponse = await fetch(`/check_video/${user_id}`);
371
- let checkResult = await checkResponse.json();
372
- if (checkResult.ready) break;
373
- await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10s before checking again
374
- }
375
- loader.classList.add("hidden");
376
- let videoUrl = `/video/${user_id}?t=${new Date().getTime()}`;
377
- outputVideo.src = videoUrl;
378
- outputVideo.load();
379
- outputVideo.play();
380
- outputVideo.setAttribute("crossOrigin", "anonymous");
381
- outputVideo.classList.remove("hidden");
382
- downloadBtn.href = videoUrl;
383
- document.getElementById("downloadBtn").style.visibility = "visible";
384
- }
385
- });
386
- document.getElementById('outputVideo').addEventListener('error', function() {
387
- console.log("⚠️ Video could not be played, showing download button instead.");
388
- document.getElementById('outputVideo').classList.add("hidden");
389
- document.getElementById("downloadBtn").style.visibility = "visible";
390
- });
391
- </script>
392
- </body>
393
- </html>
394
- """
395
-
396
  # ── Static-web ──────────────────────────────────────────────────────────
 
 
397
  app = FastAPI()
398
- app.mount("/static", StaticFiles(directory=BASE), name="static")
 
 
 
 
 
 
399
  video_ready={}
400
  @app.get("/ui", response_class=HTMLResponse)
401
- def ui(): return HTML_CONTENT
 
 
 
 
 
 
402
  def _uid(): return uuid.uuid4().hex[:8]
403
 
 
404
  # ── End-points ──────────────────────────────────────────────────────────
405
  # User upload environment img here
406
  @app.post("/upload/")
@@ -421,6 +300,84 @@ def stream(uid:str):
421
  if not os.path.exists(vid): return Response(status_code=404)
422
  return StreamingResponse(open(vid,"rb"), media_type="video/mp4")
423
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
  # ── Core pipeline (runs in background thread) ───────────────────────────
425
  def _pipeline(uid,img_path):
426
  print(f"▶️ [{uid}] processing")
@@ -559,6 +516,7 @@ def _pipeline(uid,img_path):
559
  os.remove(out_tmp); video_ready[uid]=True
560
  print(f"✅ [{uid}] video ready → {final}")
561
 
 
562
  # ── Run locally (HF Space ignores since built with Docker image) ────────
563
  if __name__=="__main__":
564
  uvicorn.run(app,host="0.0.0.0",port=7860)
 
2
 
3
  # ───────────────────────── app.py (Sall-e demo) ─────────────────────────
4
  # FastAPI ▸ upload image ▸ multi-model garbage detection ▸ ADE-20K
5
+ # semantic segmentation (Water / Garbage) ▸ A* navigation ▸ H.264 video
6
  # =======================================================================
7
 
8
  import os, uuid, threading, shutil, time, heapq, cv2, numpy as np
 
12
  from fastapi.responses import HTMLResponse, StreamingResponse, Response
13
  from fastapi.staticfiles import StaticFiles
14
 
15
+
16
  # ── Vision libs ─────────────────────────────────────────────────────────
17
  import torch, yolov5, ffmpeg
18
  from ultralytics import YOLO
 
20
  DetrImageProcessor, DetrForObjectDetection,
21
  SegformerFeatureExtractor, SegformerForSemanticSegmentation
22
  )
23
+ # from sklearn.neighbors import NearestNeighbors
24
+ from inference_sdk import InferenceHTTPClient
25
+
26
 
27
  # ── Folders / files ─────────────────────────────────────────────────────
28
  BASE = "/home/user/app"
 
48
  "nvidia/segformer-b4-finetuned-ade-512-512")
49
  segformer = SegformerForSemanticSegmentation.from_pretrained(
50
  "nvidia/segformer-b4-finetuned-ade-512-512")
51
+ model_animal = YOLO(f"{MODEL_DIR}/yolov8n.pt") # Load COCO pre-trained YOLOv8 for animal detection
52
  print("✅ Models ready\n")
53
 
54
  # ── ADE-20K palette + custom mapping (verbatim) ─────────────────────────
 
167
  cv2.drawContours(overlay, contours, -1, color, thickness=cv2.FILLED)
168
  return cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0)
169
 
170
+
171
  # ── A* and KNN over binary water grid ─────────────────────────────────
172
  def astar(start, goal, occ):
173
  h = lambda a,b: abs(a[0]-b[0])+abs(a[1]-b[1])
 
227
  todo.remove(list(best))
228
  return path
229
 
230
+
231
  # ── Robot sprite/class -──────────────────────────────────────────────────
232
  class Robot:
233
  def __init__(self, sprite, speed=2000): # Declare the robot's physical stats and routing (position, speed, movement, path)
 
257
  break
258
 
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  # ── Static-web ──────────────────────────────────────────────────────────
261
+ from fastapi.responses import JSONResponse, FileResponse
262
+ from fastapi.middleware.cors import CORSMiddleware
263
  app = FastAPI()
264
+ app.add_middleware(
265
+ CORSMiddleware,
266
+ allow_origins=["*"],
267
+ allow_methods=["*"],
268
+ allow_headers=["*"],
269
+ )
270
+ app.mount("/statics", StaticFiles(directory="statics"), name="statics")
271
  video_ready={}
272
  @app.get("/ui", response_class=HTMLResponse)
273
+ async def serve_index():
274
+ p = "statics/index.html"
275
+ if os.path.exists(p):
276
+ print("[STATIC] Serving index.html")
277
+ return FileResponse(p)
278
+ print("[STATIC] index.html not found")
279
+ return JSONResponse(status_code=404, content={"detail":"Not found"})
280
  def _uid(): return uuid.uuid4().hex[:8]
281
 
282
+
283
  # ── End-points ──────────────────────────────────────────────────────────
284
  # User upload environment img here
285
  @app.post("/upload/")
 
300
  if not os.path.exists(vid): return Response(status_code=404)
301
  return StreamingResponse(open(vid,"rb"), media_type="video/mp4")
302
 
303
+ # ─── Detect animal/wildlife ─────────────────────────────────────────────────
304
+ # Init clients
305
+ # https://universe.roboflow.com/team-hope-mmcyy/hydroquest
306
+ robo_fish = InferenceHTTPClient(
307
+ api_url="https://detect.roboflow.com",
308
+ api_key=os.getenv("ROBOFLOW_KEY", "")
309
+ )
310
+ # https://universe.roboflow.com/sky-sd2zq/bird_only-pt0bm/model/1
311
+ robo_bird = InferenceHTTPClient(
312
+ api_url="https://detect.roboflow.com",
313
+ api_key=os.getenv("ROBOFLOW_KEY", "")
314
+ )
315
+ # Animal detection endpoint (animal, fish, bird as target classes)
316
+ @app.post("/animal/")
317
+ async def detect_animals(file: UploadFile = File(...)):
318
+ img_id = _uid()
319
+ img_path = f"{UPLOAD_DIR}/{img_id}_{file.filename}"
320
+ with open(img_path, "wb") as f:
321
+ shutil.copyfileobj(file.file, f)
322
+ print(f"[Animal] Uploaded image: {img_path}")
323
+ # Read and prepare detection
324
+ image = cv2.imread(img_path)
325
+ detections = []
326
+
327
+ # 1. YOLOv8 local
328
+ print("[Animal] Detecting via YOLOv8…")
329
+ try:
330
+ results = model_animal(image)[0]
331
+ for box in results.boxes:
332
+ conf = box.conf[0].item()
333
+ if conf >= 0.75:
334
+ cls_id = int(box.cls[0].item())
335
+ label = model_animal.names[cls_id].lower()
336
+ if label in ["dog", "cat", "cow", "horse", "elephant", "bear", "zebra", "giraffe", "bird"]:
337
+ x1, y1, x2, y2 = map(int, box.xyxy[0].tolist())
338
+ detections.append(((x1, y1, x2, y2), "Animal Alert"))
339
+ except Exception as e:
340
+ print("[YOLOv8 Error]", e)
341
+
342
+ # 2. Roboflow Fish
343
+ try:
344
+ print("[Animal] Detecting via Roboflow Fish model…")
345
+ fish_preds = robo_fish.infer(img_path, model_id="hydroquest/1")
346
+ for pred in fish_preds.get("predictions", []):
347
+ if pred["confidence"] >= 0.75:
348
+ x1 = int(pred["x"] - pred["width"] / 2)
349
+ y1 = int(pred["y"] - pred["height"] / 2)
350
+ x2 = int(pred["x"] + pred["width"] / 2)
351
+ y2 = int(pred["y"] + pred["height"] / 2)
352
+ detections.append(((x1, y1, x2, y2), "Fish Alert"))
353
+ except Exception as e:
354
+ print("[Roboflow Fish Error]", e)
355
+
356
+ # 3. Roboflow Bird
357
+ try:
358
+ print("[Animal] Detecting via Roboflow Bird model…")
359
+ bird_preds = robo_bird.infer(img_path, model_id="bird_only-pt0bm/1")
360
+ for pred in bird_preds.get("predictions", []):
361
+ if pred["confidence"] >= 0.75:
362
+ x1 = int(pred["x"] - pred["width"] / 2)
363
+ y1 = int(pred["y"] - pred["height"] / 2)
364
+ x2 = int(pred["x"] + pred["width"] / 2)
365
+ y2 = int(pred["y"] + pred["height"] / 2)
366
+ detections.append(((x1, y1, x2, y2), "Bird Alert"))
367
+ except Exception as e:
368
+ print("[Roboflow Bird Error]", e)
369
+ # Count detection
370
+ print(f"[Animal] Total detections: {len(detections)}")
371
+ # Write label
372
+ for (x1, y1, x2, y2), label in detections:
373
+ cv2.rectangle(image, (x1, y1), (x2, y2), (0, 0, 255), 2)
374
+ cv2.putText(image, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
375
+ # Write img
376
+ result_path = f"{OUTPUT_DIR}/{img_id}_animal.jpg"
377
+ cv2.imwrite(result_path, image)
378
+ return FileResponse(result_path, media_type="image/jpeg")
379
+
380
+
381
  # ── Core pipeline (runs in background thread) ───────────────────────────
382
  def _pipeline(uid,img_path):
383
  print(f"▶️ [{uid}] processing")
 
516
  os.remove(out_tmp); video_ready[uid]=True
517
  print(f"✅ [{uid}] video ready → {final}")
518
 
519
+
520
  # ── Run locally (HF Space ignores since built with Docker image) ────────
521
  if __name__=="__main__":
522
  uvicorn.run(app,host="0.0.0.0",port=7860)
requirements.txt CHANGED
@@ -15,6 +15,7 @@ yolov5
15
  huggingface_hub>=0.20.3
16
  transformers==4.37.2
17
  accelerate==0.27.2
 
18
 
19
  # Video Processing
20
  ffmpeg-python
 
15
  huggingface_hub>=0.20.3
16
  transformers==4.37.2
17
  accelerate==0.27.2
18
+ inference-sdk
19
 
20
  # Video Processing
21
  ffmpeg-python
icon.png → statics/icon.png RENAMED
File without changes
statics/index.html ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Sall-e Garbage Detection</title>
5
+ <link rel="website icon" type="png" href="/statics/icon.png" >
6
+ <link rel="stylesheet" href="/statics/style.css">
7
+ </head>
8
+ <body>
9
+ <h1>Upload an Image to Simulate Garbage Detection and Robot Navigation</h1>
10
+ <div id="upload-container">
11
+ <input type="file" id="upload" accept="image/*">
12
+ </div>
13
+ <div id="loader" class="loader hidden">
14
+ <div id="spinner"></div>
15
+ <!-- <p>Garbage detection model processing...</p> -->
16
+ </div>
17
+ <video id="outputVideo" class="outputVideo" controls></video>
18
+ <a id="downloadBtn" class="downloadBtn">Download Video</a>
19
+ <h1>Upload an Image to Simulate Front-view Animal Detection</h1>
20
+ <div id="upload-container2">
21
+ <input type="file" id="upload2" accept="image/*">
22
+ <button onclick="uploadAnimal()">Check Animal</button>
23
+ </div>
24
+ <div id="animal-result"></div>
25
+ <script src="/statics/script.js"></script>
26
+ </body>
27
+ </html>
statics/script.js ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ document.addEventListener("DOMContentLoaded", function() {
3
+ document.getElementById("outputVideo").classList.add("hidden");
4
+ document.getElementById("downloadBtn").style.visibility = "hidden";
5
+ });
6
+ document.getElementById('upload').addEventListener('change', async function(event) {
7
+ event.preventDefault();
8
+ const loader = document.getElementById("loader");
9
+ const outputVideo = document.getElementById("outputVideo");
10
+ const downloadBtn = document.getElementById("downloadBtn");
11
+ let file = event.target.files[0];
12
+ if (file) {
13
+ let formData = new FormData();
14
+ formData.append("file", file);
15
+ loader.classList.remove("hidden");
16
+ outputVideo.classList.add("hidden");
17
+ document.getElementById("downloadBtn").style.visibility = "hidden";
18
+ let response = await fetch('/upload/', { method: 'POST', body: formData });
19
+ let result = await response.json();
20
+ let user_id = result.user_id;
21
+ while (true) {
22
+ let checkResponse = await fetch(`/check_video/${user_id}`);
23
+ let checkResult = await checkResponse.json();
24
+ if (checkResult.ready) break;
25
+ await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10s before checking again
26
+ }
27
+ loader.classList.add("hidden");
28
+ let videoUrl = `/video/${user_id}?t=${new Date().getTime()}`;
29
+ outputVideo.src = videoUrl;
30
+ outputVideo.load();
31
+ outputVideo.play();
32
+ outputVideo.setAttribute("crossOrigin", "anonymous");
33
+ outputVideo.classList.remove("hidden");
34
+ downloadBtn.href = videoUrl;
35
+ document.getElementById("downloadBtn").style.visibility = "visible";
36
+ }
37
+ });
38
+ document.getElementById('outputVideo').addEventListener('error', function() {
39
+ console.log("⚠️ Video could not be played, showing download button instead.");
40
+ document.getElementById('outputVideo').classList.add("hidden");
41
+ document.getElementById("downloadBtn").style.visibility = "visible";
42
+ });
43
+
44
+ async function uploadAnimal() {
45
+ const fileInput = document.getElementById('upload2');
46
+ if (!fileInput.files.length) return alert("Upload an image first");
47
+ // Upload and read image file
48
+ const formData = new FormData();
49
+ formData.append("file", fileInput.files[0]);
50
+ // Handshake with FastAPI
51
+ const res = await fetch("/animal/", {
52
+ method: "POST",
53
+ body: formData
54
+ });
55
+ // Error
56
+ if (!res.ok) {
57
+ alert("Failed to process animal detection.");
58
+ return;
59
+ }
60
+ // Create image
61
+ const blob = await res.blob();
62
+ const imgURL = URL.createObjectURL(blob);
63
+ document.getElementById("animal-result").innerHTML =
64
+ `<p><b>Animal Detection Result:</b></p><img src="${imgURL}" width="640"/>`;
65
+ }
66
+
statics/style.css ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ font-family: 'Roboto', sans-serif; background: linear-gradient(270deg, rgb(44, 13, 58), rgb(13, 58, 56)); color: white; text-align: center; margin: 0; padding: 50px;
3
+ }
4
+ h1 {
5
+ font-size: 40px;
6
+ background: linear-gradient(to right, #f32170, #ff6b08, #cf23cf, #eedd44);
7
+ -webkit-text-fill-color: transparent;
8
+ -webkit-background-clip: text;
9
+ font-weight: bold;
10
+ }
11
+ #upload-container, #upload-container2 {
12
+ background: rgba(255, 255, 255, 0.2); padding: 20px; width: 70%; border-radius: 10px; display: inline-block; box-shadow: 0px 0px 10px rgba(255, 255, 255, 0.3);
13
+ }
14
+ #upload, #upload2 {
15
+ font-size: 18px; padding: 10px; border-radius: 5px; border: none; background: #fff; cursor: pointer;
16
+ }
17
+ #loader {
18
+ margin-top: 10px; margin-left: auto; margin-right: auto; width: 60px; height: 60px; font-size: 12px; text-align: center;
19
+ }
20
+ p {
21
+ margin-top: 10px; font-size: 12px; color: #3498db;
22
+ }
23
+ #spinner {
24
+ border: 8px solid #f3f3f3; border-top: 8px solid rgb(117 7 7); border-radius: 50%; animation: spin 1s linear infinite; width: 40px; height: 40px; margin: auto;
25
+ }
26
+ @keyframes spin {
27
+ 0% { transform: rotate(0deg); }
28
+ 100% { transform: rotate(360deg); }
29
+ }
30
+ #outputVideo {
31
+ margin-top: 20px; width: 70%; margin-left: auto; margin-right: auto; max-width: 640px; border-radius: 10px; box-shadow: 0px 0px 10px rgba(255, 255, 255, 0.3);
32
+ }
33
+ #downloadBtn {
34
+ display: block; visibility: hidden; width: 20%; margin-top: 20px; margin-left: auto; margin-right: auto; padding: 10px 15px; font-size: 16px; background: #27ae60; color: white; border: none; border-radius: 5px; cursor: pointer; text-decoration: none;
35
+ }
36
+ #downloadBtn:hover {
37
+ background: #950606;
38
+ }
39
+ .hidden {
40
+ display: none;
41
+ }
42
+ @media (max-width: 860px) {
43
+ h1 { font-size: 30px; }
44
+ }
45
+ @media (max-width: 720px) {
46
+ h1 { font-size: 25px; }
47
+ #upload { font-size: 15px; }
48
+ #downloadBtn { font-size: 13px; }
49
+ }
50
+ @media (max-width: 580px) {
51
+ h1 { font-size: 20px; }
52
+ #upload { font-size: 10px; }
53
+ #downloadBtn { font-size: 10px; }
54
+ }
55
+ @media (max-width: 580px) {
56
+ h1 { font-size: 10px; }
57
+ }
58
+ @media (max-width: 460px) {
59
+ #upload { font-size: 7px; }
60
+ }
61
+ @media (max-width: 400px) {
62
+ h1 { font-size: 14px; }
63
+ }
64
+ @media (max-width: 370px) {
65
+ h1 { font-size: 11px; }
66
+ #upload { font-size: 5px; }
67
+ #downloadBtn { font-size: 7px; }
68
+ }
69
+ @media (max-width: 330px) {
70
+ h1 { font-size: 8px; }
71
+ #upload { font-size: 3px; }
72
+ #downloadBtn { font-size: 5px; }
73
+ }