jmfinizio commited on
Commit
d8afa61
·
0 Parent(s):

Fresh start

Browse files
.DS_Store ADDED
Binary file (8.2 kB). View file
 
.gitattributes ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ ffmpeg filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
2
+ # you will also find guides on how best to write your Dockerfile
3
+
4
+ FROM python:3.9
5
+
6
+ RUN useradd -m -u 1000 user
7
+ USER user
8
+ ENV PATH="/home/user/.local/bin:$PATH"
9
+
10
+ WORKDIR /app
11
+
12
+ COPY --chown=user ./requirements.txt requirements.txt
13
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
14
+
15
+ COPY --chown=user . /app
16
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: CISS Web App
3
+ emoji: 📊
4
+ colorFrom: red
5
+ colorTo: yellow
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
analysis_output/session_33d6d79f-952c-476a-9f36-cd5fdea84d3c/analysis.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ timestamp,second,proximity to parent,proximity to stranger,fear,freeze
2
+ 00:07:37,0,1,1,2,1
3
+ 00:07:38,1,1,1,2,1
4
+ 00:07:39,2,1,1,2,1
5
+ 00:07:40,3,1,1,2,1
6
+ 00:07:41,4,1,1,2,1
analysis_output/session_4c97bc51-b190-4205-b4dd-f9fc2cd9fc15/analysis.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ timestamp,second,proximity to parent,proximity to stranger,fear,freeze
2
+ 00:07:37,0,1,1,2,1
3
+ 00:07:38,1,1,1,2,1
4
+ 00:07:39,2,1,1,2,1
5
+ 00:07:40,3,1,1,2,1
6
+ 00:07:41,4,1,1,2,1
analysis_output/session_4cfe63bb-d56d-4457-8d2d-6e85af137d66/analysis.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ timestamp,second,proximity to parent,proximity to stranger,fear,freeze
2
+ 00:07:37,0,1,1,2,1
3
+ 00:07:38,1,1,1,2,1
4
+ 00:07:39,2,1,1,2,1
5
+ 00:07:40,3,1,1,2,1
6
+ 00:07:41,4,1,1,2,1
analysis_output/session_bb31a607-52b9-495f-aeb1-346c8f87bee1/analysis.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ timestamp,second,proximity to parent,proximity to stranger,fear,freeze
2
+ 00:07:37,0,1,1,2,1
3
+ 00:07:38,1,1,1,2,1
4
+ 00:07:39,2,1,1,2,1
5
+ 00:07:40,3,1,1,2,1
6
+ 00:07:41,4,1,1,2,1
analysis_output/session_d7af8070-871b-41bd-b611-fd2bd9773404/analysis.csv ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ timestamp,second,proximity to parent,proximity to stranger,fear,freeze
2
+ 00:05:58,0,1,1,0,
3
+ 00:05:59,1,1,1,0,
4
+ 00:06:00,2,1,1,0,
5
+ 00:06:01,3,1,1,2,
6
+ 00:06:02,4,1,1,0,
7
+ 00:06:03,5,1,1,0,
8
+ 00:06:04,6,1,1,0,
9
+ 00:06:05,7,1,1,0,
10
+ 00:06:06,8,1,1,0,
11
+ 00:06:07,9,1,1,0,
12
+ 00:06:08,10,1,1,0,
13
+ 00:06:09,11,1,1,0,
14
+ 00:06:10,12,1,1,0,
15
+ 00:06:11,13,1,1,0,
16
+ 00:06:12,14,1,1,2,
17
+ 00:06:13,15,1,1,0,
18
+ 00:06:14,16,1,1,0,
19
+ 00:06:15,17,1,1,0,
20
+ 00:06:16,18,1,1,2,0
21
+ 00:06:17,19,1,1,2,0
22
+ 00:06:18,20,1,1,2,0
23
+ 00:06:19,21,1,1,0,
24
+ 00:06:20,22,1,1,2,
25
+ 00:06:21,23,1,1,0,
26
+ 00:06:22,24,1,1,0,
27
+ 00:06:23,25,1,1,0,
28
+ 00:06:24,26,1,1,2,
29
+ 00:06:25,27,1,1,0,
30
+ 00:06:26,28,1,1,0,
31
+ 00:06:27,29,1,1,0,
32
+ 00:06:28,30,1,1,2,0
33
+ 00:06:29,31,1,1,2,0
34
+ 00:06:30,32,1,1,2,0
35
+ 00:06:31,33,1,1,0,
36
+ 00:06:32,34,1,1,2,0
37
+ 00:06:33,35,1,1,2,0
38
+ 00:06:34,36,1,1,0,
39
+ 00:06:35,37,1,1,0,
40
+ 00:06:36,38,1,1,0,
41
+ 00:06:37,39,1,1,2,
42
+ 00:06:38,40,1,1,0,
43
+ 00:06:39,41,1,1,0,
44
+ 00:06:40,42,1,1,0,
45
+ 00:06:41,43,1,1,0,
46
+ 00:06:42,44,1,1,2,
47
+ 00:06:43,45,1,1,0,
48
+ 00:06:44,46,1,1,0,
49
+ 00:06:45,47,1,1,0,
50
+ 00:06:46,48,1,1,0,
51
+ 00:06:47,49,1,1,2,
52
+ 00:06:48,50,1,1,0,
53
+ 00:06:49,51,1,1,0,
54
+ 00:06:50,52,1,1,0,
55
+ 00:06:51,53,1,1,0,
56
+ 00:06:52,54,1,1,0,
57
+ 00:06:53,55,1,1,0,
58
+ 00:06:54,56,1,1,2,0
59
+ 00:06:55,57,1,1,2,0
60
+ 00:06:56,58,1,1,0,
61
+ 00:06:57,59,1,1,0,
62
+ 00:06:58,60,1,1,2,
63
+ 00:06:59,61,1,1,0,
64
+ 00:07:00,62,1,1,0,
65
+ 00:07:01,63,1,1,0,
66
+ 00:07:02,64,1,1,0,
67
+ 00:07:03,65,1,1,0,
68
+ 00:07:04,66,1,1,0,
69
+ 00:07:05,67,1,1,0,
70
+ 00:07:06,68,1,1,0,
71
+ 00:07:07,69,1,1,2,
72
+ 00:07:08,70,1,1,0,
73
+ 00:07:09,71,1,1,0,
74
+ 00:07:10,72,1,1,0,
75
+ 00:07:11,73,1,1,0,
76
+ 00:07:12,74,1,1,0,
77
+ 00:07:13,75,1,1,0,
78
+ 00:07:14,76,1,1,0,
79
+ 00:07:15,77,1,1,0,
80
+ 00:07:16,78,1,1,0,
81
+ 00:07:17,79,1,1,0,
82
+ 00:07:18,80,1,1,0,
83
+ 00:07:19,81,1,1,2,1
84
+ 00:07:20,82,1,1,2,1
85
+ 00:07:21,83,1,1,0,
86
+ 00:07:22,84,1,1,0,
87
+ 00:07:23,85,1,1,0,
88
+ 00:07:24,86,1,1,0,
89
+ 00:07:25,87,1,1,0,
90
+ 00:07:26,88,1,1,2,0
91
+ 00:07:27,89,1,1,2,0
92
+ 00:07:28,90,1,1,0,
93
+ 00:07:29,91,1,1,0,
94
+ 00:07:30,92,1,1,0,
95
+ 00:07:31,93,,,,
96
+ 00:07:32,94,0,2,0,
97
+ 00:07:33,95,0,2,1,
98
+ 00:07:34,96,0,2,0,
99
+ 00:07:35,97,,,,
100
+ 00:07:36,98,,,,
101
+ 00:07:37,99,1,1,0,
102
+ 00:07:38,100,1,1,0,
103
+ 00:07:39,101,1,1,0,
104
+ 00:07:40,102,1,1,0,
105
+ 00:07:41,103,1,1,0,
106
+ 00:07:42,104,1,1,1,0
107
+ 00:07:43,105,1,1,2,0
108
+ 00:07:44,106,1,1,2,0
109
+ 00:07:45,107,1,1,2,1
110
+ 00:07:46,108,1,1,2,1
111
+ 00:07:47,109,1,1,0,
112
+ 00:07:48,110,1,1,0,
113
+ 00:07:49,111,1,1,0,
114
+ 00:07:50,112,1,1,0,
115
+ 00:07:51,113,1,1,0,
116
+ 00:07:52,114,1,1,0,
117
+ 00:07:53,115,1,1,1,
118
+ 00:07:54,116,1,1,0,
119
+ 00:07:55,117,1,1,2,1
120
+ 00:07:56,118,1,1,2,1
121
+ 00:07:57,119,1,1,0,
122
+ 00:07:58,120,1,1,2,0
123
+ 00:07:59,121,1,1,2,0
124
+ 00:08:00,122,1,1,2,0
125
+ 00:08:01,123,1,1,0,
126
+ 00:08:02,124,1,1,0,
127
+ 00:08:03,125,1,1,0,
128
+ 00:08:04,126,1,1,0,
129
+ 00:08:05,127,1,1,0,
130
+ 00:08:06,128,1,1,0,
131
+ 00:08:07,129,1,1,0,
132
+ 00:08:08,130,1,1,0,
133
+ 00:08:09,131,1,1,0,
134
+ 00:08:10,132,1,1,0,
135
+ 00:08:11,133,1,1,2,0
136
+ 00:08:12,134,1,1,2,0
137
+ 00:08:13,135,1,1,2,0
138
+ 00:08:14,136,1,1,0,
139
+ 00:08:15,137,1,1,0,
140
+ 00:08:16,138,1,1,2,
141
+ 00:08:17,139,1,1,0,
142
+ 00:08:18,140,1,1,2,0
143
+ 00:08:19,141,1,1,2,0
144
+ 00:08:20,142,1,1,0,
145
+ 00:08:21,143,1,1,0,
146
+ 00:08:22,144,1,1,2,
analysis_output/session_ee8801d4-2515-4873-9db6-a8be6180e836/analysis.csv ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ timestamp,second,proximity to parent,proximity to stranger,fear,freeze
2
+ 00:07:37,0,1,1,0,
3
+ 00:07:38,1,1,1,0,
4
+ 00:07:39,2,1,1,0,
5
+ 00:07:40,3,1,1,0,
6
+ 00:07:41,4,1,1,0,
app.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from backend.main import app
backend/.DS_Store ADDED
Binary file (6.15 kB). View file
 
backend/__init__.py ADDED
File without changes
backend/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (155 Bytes). View file
 
backend/__pycache__/main.cpython-310.pyc ADDED
Binary file (23.6 kB). View file
 
backend/main.py ADDED
@@ -0,0 +1,911 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import datetime
2
+ import os
3
+ import cv2
4
+ import uuid
5
+ import json
6
+ import time
7
+ import re
8
+ import subprocess
9
+ import uuid
10
+ import asyncio
11
+ import joblib
12
+ import logging
13
+ import numpy as np
14
+ import pandas as pd
15
+ import tempfile
16
+ import warnings
17
+ import shutil
18
+ from pathlib import Path
19
+ from PIL import Image
20
+ import ffmpeg
21
+ import torch
22
+ import torchvision.transforms as T
23
+ from ultralytics import YOLO
24
+ import mediapipe as mp
25
+ from fastapi import FastAPI, UploadFile, File, HTTPException, BackgroundTasks, Form, Request
26
+ from fastapi.responses import FileResponse, StreamingResponse, JSONResponse
27
+ from fastapi.middleware.cors import CORSMiddleware
28
+ from fastapi.staticfiles import StaticFiles
29
+ from backend.midas_utils.transforms import Compose, Resize, NormalizeImage, PrepareForNet
30
+
31
+ #################################################
32
+ # Initialize application
33
+ #################################################
34
+ torch.serialization.add_safe_globals([
35
+ torch.nn.modules.conv.Conv2d,
36
+ torch.nn.modules.batchnorm.BatchNorm2d,
37
+ torch.nn.modules.linear.Linear,
38
+ torch.nn.modules.container.Sequential,
39
+ torch.nn.modules.activation.SiLU,
40
+ torch.nn.modules.container.ModuleList,
41
+ torch.nn.modules.upsampling.Upsample,
42
+ torch.nn.modules.pooling.MaxPool2d
43
+ ])
44
+
45
+
46
+ logger = logging.getLogger(__name__)
47
+ logging.basicConfig(level=logging.INFO)
48
+
49
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
50
+
51
+ app = FastAPI()
52
+
53
+ # CORS Configuration
54
+ app.add_middleware(
55
+ CORSMiddleware,
56
+ allow_origins=["*"],
57
+ allow_credentials=True,
58
+ allow_methods=["*"],
59
+ allow_headers=["*"],
60
+ )
61
+
62
+
63
+ # Serve frontend files
64
+ static_dir = Path(__file__).parent.parent / "frontend" / "static"
65
+ app.mount("/static", StaticFiles(directory=static_dir), name="static")
66
+
67
+ # Configuration
68
+ DETECTION_MODEL_PATH = Path(__file__).parent / 'models' / "yolo_retrained_model.pt"
69
+ POSE_MODEL_PATH = Path(__file__).parent / 'models' / "yolov8n-pose.pt"
70
+ MAX_VIDEO_SIZE = 500 * 1024 * 1024
71
+ OUTPUT_DIR = Path("analysis_output")
72
+ UPLOADED_VIDEOS = {} # Track uploaded video session
73
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
74
+
75
+ # Global state
76
+ PROGRESS_STORE = {}
77
+ ANALYSIS_ACTIVE = False
78
+
79
+ @app.middleware("http")
80
+ async def error_handling_middleware(request: Request, call_next):
81
+ try:
82
+ return await call_next(request)
83
+ except Exception as e:
84
+ logger.error(f"Unexpected error: {str(e)}")
85
+ return JSONResponse(
86
+ status_code=500,
87
+ content={"message": "Internal server error"}
88
+ )
89
+
90
+ @app.on_event("startup")
91
+ async def initialize_models():
92
+ """Initialize models with warmup inference"""
93
+
94
+ try:
95
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
96
+ logger.info(f"Initializing models on {device}")
97
+
98
+ # Initialize detection model
99
+ app.state.detection_model = YOLO(DETECTION_MODEL_PATH).to(device)
100
+ dummy = np.zeros((640, 640, 3), dtype=np.uint8)
101
+ app.state.detection_model(dummy, verbose=False) # Warmup
102
+
103
+ # Initialize pose model
104
+ app.state.pose_model = YOLO(POSE_MODEL_PATH).to(device)
105
+ app.state.pose_model(dummy, verbose=False) # Warmup
106
+
107
+ logger.info("Models initialized successfully")
108
+ except Exception as e:
109
+ logger.error(f"Model initialization failed: {str(e)}")
110
+ raise RuntimeError(f"Model initialization failed: {str(e)}")
111
+
112
+ def update_progress(process_id: str, current: int, total: int, message: str):
113
+ """Update progress store with analysis status"""
114
+ PROGRESS_STORE[process_id] = {
115
+ "percent": min(100, (current / total) * 100),
116
+ "message": message,
117
+ "current": current,
118
+ "total": total,
119
+ "status": "processing"
120
+ }
121
+
122
+ #################################################
123
+ # Initialize Models
124
+ #################################################
125
+
126
+ # Child detection and image cropping
127
+ def detect_child_and_crop(frame):
128
+ try:
129
+ results = app.state.detection_model.predict(frame, verbose=False)[0]
130
+ class_ids = results.boxes.cls.cpu().numpy()
131
+ confidences = results.boxes.conf.cpu().numpy()
132
+ bboxes = results.boxes.xyxy.cpu().numpy()
133
+ child_bbox = None
134
+
135
+ for box, cls, conf in zip(bboxes, class_ids, confidences):
136
+ if conf > 0.6:
137
+ if cls == 1:
138
+ child_bbox = box
139
+ elif cls == 0:
140
+ adult_bbox = box
141
+ elif cls == 2:
142
+ stranger_bbox = box
143
+
144
+ if child_bbox is None:
145
+ return None
146
+
147
+ x1, y1, x2, y2 = map(int, child_bbox)
148
+ # Validate and clamp coordinates
149
+ x1 = max(0, x1)
150
+ y1 = max(0, y1)
151
+ x2 = min(frame.shape[1], x2)
152
+ y2 = min(frame.shape[0], y2)
153
+ if x1 >= x2 or y1 >= y2:
154
+ logger.warning("Invalid child bounding box")
155
+ return None
156
+
157
+ child_roi = frame[y1:y2, x1:x2]
158
+ if child_roi.size == 0:
159
+ logger.warning("Empty child ROI")
160
+ return None
161
+
162
+ return child_roi
163
+
164
+ except Exception as e:
165
+ logger.error(f"Detection error: {str(e)}")
166
+ return None
167
+
168
+ def load_depth_model():
169
+ try:
170
+ with warnings.catch_warnings():
171
+ warnings.simplefilter("ignore")
172
+ model = torch.hub.load(
173
+ 'intel-isl/MiDaS',
174
+ 'MiDaS_small',
175
+ pretrained=True,
176
+ trust_repo=True
177
+ ).float()
178
+ model.eval().to(device)
179
+ print("Successfully loaded MiDaS model from torch.hub")
180
+ return model
181
+ except Exception as e:
182
+ raise RuntimeError(f"Failed to load MiDaS model: {e}")
183
+
184
+ # Load transforms
185
+ midas_transforms = torch.hub.load("intel-isl/MiDaS", "transforms")
186
+ Resize = midas_transforms.Resize
187
+ NormalizeImage = midas_transforms.NormalizeImage
188
+ PrepareForNet = midas_transforms.PrepareForNet
189
+
190
+ # Define transform pipeline
191
+ transform_pipeline = T.Compose([
192
+ lambda img: {"image": np.array(img.convert("RGB"), dtype=np.float32) / 255.0},
193
+ Resize(
194
+ 256, 256, resize_target=None, keep_aspect_ratio=True,
195
+ ensure_multiple_of=32, resize_method="upper_bound",
196
+ image_interpolation_method=cv2.INTER_CUBIC
197
+ ),
198
+ NormalizeImage(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
199
+ PrepareForNet(),
200
+ lambda sample: torch.from_numpy(sample["image"]),
201
+ ])
202
+
203
+ # Load model once
204
+ depth_model = load_depth_model()
205
+
206
+ def calculate_distance_between_objects(frame, obj1_label, obj2_label):
207
+ results = app.state.detection_model.predict(frame, verbose=False)[0]
208
+ labels = results.names if hasattr(results, 'names') else {}
209
+
210
+ obj1_center = None
211
+ obj2_center = None
212
+
213
+ for box in results.boxes:
214
+ cls = int(box.cls[0].item())
215
+ label = labels.get(cls, str(cls))
216
+
217
+ x1, y1, x2, y2 = map(int, box.xyxy[0].cpu().numpy())
218
+ center = ((x1 + x2) // 2, (y1 + y2) // 2)
219
+
220
+ if label.lower() == obj1_label.lower():
221
+ obj1_center = center
222
+ elif label.lower() == obj2_label.lower():
223
+ obj2_center = center
224
+
225
+ # Validation checks with proper error handling
226
+ if obj1_center is None:
227
+ print(f"Important warning: {obj1_label} not detected.")
228
+ return None
229
+
230
+ if obj2_center is None:
231
+ if obj2_label.lower() != "stranger":
232
+ print(f"Warning: {obj2_label} not detected.")
233
+ return None
234
+
235
+ # Add coordinate validation
236
+ def validate_coord(coord):
237
+ return isinstance(coord, tuple) and len(coord) == 2 and \
238
+ all(isinstance(v, (int, float)) for v in coord)
239
+
240
+ if not validate_coord(obj1_center) or not validate_coord(obj2_center):
241
+ print("Invalid coordinates detected")
242
+ return None
243
+
244
+ try:
245
+ # Estimate depth
246
+ img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
247
+ img_pil = Image.fromarray(img_rgb) # Convert to PIL Image first
248
+ input_tensor = transform_pipeline(img_pil).to(device)
249
+
250
+ if input_tensor.dim() == 3:
251
+ input_tensor = input_tensor.unsqueeze(0)
252
+ input_tensor = input_tensor.to(device)
253
+
254
+ with torch.no_grad():
255
+ output = depth_model(input_tensor)
256
+ depth_map = output.squeeze().cpu().numpy()
257
+
258
+ # Rescale object centers with safety checks
259
+ original_h, original_w = frame.shape[:2]
260
+ depth_h, depth_w = depth_map.shape
261
+
262
+ def safe_scale(coord, orig_dim, target_dim):
263
+ try:
264
+ return int((coord / orig_dim) * target_dim)
265
+ except ZeroDivisionError:
266
+ return 0
267
+
268
+ # Corrected scaling calls
269
+ x1 = safe_scale(obj1_center[0], original_w, depth_w)
270
+ y1 = safe_scale(obj1_center[1], original_h, depth_h)
271
+ x2 = safe_scale(obj2_center[0], original_w, depth_w)
272
+ y2 = safe_scale(obj2_center[1], original_h, depth_h)
273
+
274
+ # Depth calculation with bounds checking
275
+ def get_depth(x, y):
276
+ x = max(0, min(depth_w-1, x))
277
+ y = max(0, min(depth_h-1, y))
278
+ return depth_map[y, x]
279
+
280
+ d1 = get_depth(x1, y1)
281
+ d2 = get_depth(x2, y2)
282
+
283
+ if d1 <= 0 or d2 <= 0:
284
+ return None
285
+
286
+ # 3D coordinate conversion
287
+ fx = fy = 1109 # Focal length assumption
288
+ cx, cy = depth_w // 2, depth_h // 2
289
+
290
+ point1 = (
291
+ (x1 - cx) * d1 / fx,
292
+ (y1 - cy) * d1 / fy,
293
+ d1
294
+ )
295
+ point2 = (
296
+ (x2 - cx) * d2 / fx,
297
+ (y2 - cy) * d2 / fy,
298
+ d2
299
+ )
300
+
301
+ return float(np.linalg.norm(np.array(point1) - np.array(point2)))
302
+
303
+ except Exception as e:
304
+ logger.error(f"Distance calculation error: {str(e)}")
305
+ return None
306
+
307
+ # MediaPipe initialization
308
+ mp_face_mesh = mp.solutions.face_mesh
309
+ face_mesh = mp_face_mesh.FaceMesh(
310
+ static_image_mode=False,
311
+ max_num_faces=1,
312
+ min_detection_confidence=0.5
313
+ )
314
+
315
+ LANDMARKS = {
316
+ "left_eye": [33, 133, 159, 145, 160, 144],
317
+ "right_eye": [362, 263, 386, 374, 387, 373],
318
+ "left_eyebrow": [70, 63, 105],
319
+ "right_eyebrow": [300, 293, 334],
320
+ "mouth": [13, 14, 78, 308],
321
+ "jaw": [152]
322
+ }
323
+
324
+ def facial_keypoints(image, prev_landmarks=None):
325
+ if image is None:
326
+ logger.error("Received None frame")
327
+ return 0, None
328
+ try:
329
+ h, w = image.shape[:2]
330
+ except AttributeError:
331
+ logger.error("Invalid image type")
332
+ return 0, None
333
+ if h == 0 or w == 0 or image.size == 0:
334
+ logger.error("Received empty frame")
335
+ return 0, None
336
+
337
+ try:
338
+ results = face_mesh.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
339
+ if not results.multi_face_landmarks:
340
+ return 0, None
341
+
342
+ current_landmarks = {}
343
+ for key, indices in LANDMARKS.items():
344
+ current_landmarks[key] = [
345
+ (int(lm.x * image.shape[1]), int(lm.y * image.shape[0]))
346
+ for lm in [results.multi_face_landmarks[0].landmark[i] for i in indices]
347
+ ]
348
+
349
+ movement_score = 0
350
+ if prev_landmarks:
351
+ total_diff = sum(
352
+ np.sqrt((cx - px)**2 + (cy - py)**2)
353
+ for key in LANDMARKS
354
+ for (px, py), (cx, cy) in zip(prev_landmarks.get(key, []), current_landmarks.get(key, []))
355
+ )
356
+ valid_points = sum(len(landmarks) for landmarks in current_landmarks.values())
357
+ movement_score = 2 if (total_diff/valid_points) > 6 else 1 if (total_diff/valid_points) > 3 else 0
358
+
359
+ return movement_score, current_landmarks
360
+ except Exception as e:
361
+ logger.error(f"Facial processing error: {str(e)}")
362
+ return 0, None
363
+
364
+ def process_pose(image):
365
+ if image is None:
366
+ return None
367
+ try:
368
+ results = app.state.pose_model(image, verbose=False)
369
+ if results and hasattr(results[0], 'keypoints'):
370
+ return results[0].keypoints.xy[0].cpu().numpy()
371
+ return None
372
+ except Exception as e:
373
+ logger.error(f"Pose processing error: {str(e)}")
374
+ return None
375
+
376
+ def calculate_body_movement(current_pose, previous_pose):
377
+ if current_pose is None or previous_pose is None:
378
+ return 0.0
379
+
380
+ valid_points = 0
381
+ total_movement = 0.0
382
+
383
+ for prev, curr in zip(previous_pose, current_pose):
384
+ if not (np.isnan(prev).any() or np.isnan(curr).any()):
385
+ valid_points += 1
386
+ total_movement += abs(np.linalg.norm(curr - prev))
387
+
388
+ return total_movement
389
+
390
+ #################################################
391
+ # Preparing for Video Processing
392
+ #################################################
393
+
394
+ def time_to_seconds(timestamp):
395
+ return sum(x * int(t) for x, t in zip([3600, 60, 1], timestamp.split(':')))
396
+
397
+ def format_progress_message(stage, current, total, extras=None):
398
+ base = f"{stage} - Frame {current}/{total}"
399
+ if extras:
400
+ return f"{base} - {', '.join(f'{k}: {v}' for k,v in extras.items())}"
401
+ return base
402
+
403
+ def crop_video(process_id: str, video_path: str, timestamp1: str, timestamp2: str,
404
+ timestamp3: str, temp_dir: str, ffmpeg_path: str = 'ffmpeg') -> tuple[str, str]:
405
+ """
406
+ Crop the video into two clips with cancellation support
407
+ """
408
+ temp_dir_path = Path(temp_dir)
409
+
410
+ # Create temp directory if it doesn't exist
411
+ temp_dir_path.mkdir(parents=True, exist_ok=True)
412
+
413
+ # Generate temporary filenames
414
+ first_clip_path = temp_dir_path / f"clip1_{uuid.uuid4()}.mp4"
415
+ second_clip_path = temp_dir_path / f"clip2_{uuid.uuid4()}.mp4"
416
+
417
+ def check_cancellation():
418
+ """Check if processing was cancelled (replace with your actual progress store)"""
419
+ # You'll need to import or access your PROGRESS_STORE here
420
+ if PROGRESS_STORE.get(process_id, {}).get('status') == 'cancelled':
421
+ raise asyncio.CancelledError("Processing cancelled by user during video cropping")
422
+
423
+ def run_ffmpeg_with_cancel_check(command: list, output_file: Path) -> None:
424
+ """Run ffmpeg command with cancellation checks"""
425
+ try:
426
+ # Start the process
427
+ process = subprocess.Popen(
428
+ command,
429
+ stdout=subprocess.PIPE,
430
+ stderr=subprocess.PIPE,
431
+ universal_newlines=True
432
+ )
433
+
434
+ # Poll process while checking for cancellation
435
+ while True:
436
+ check_cancellation()
437
+ if process.poll() is not None: # Process finished
438
+ break
439
+ time.sleep(0.5) # Check every 500ms
440
+
441
+ # Check final status
442
+ if process.returncode != 0:
443
+ raise subprocess.CalledProcessError(
444
+ process.returncode,
445
+ command,
446
+ output=process.stdout,
447
+ stderr=process.stderr
448
+ )
449
+
450
+ except asyncio.CancelledError:
451
+ # Cleanup and terminate process
452
+ if process.poll() is None: # Still running
453
+ process.terminate()
454
+ try:
455
+ process.wait(timeout=5)
456
+ except subprocess.TimeoutExpired:
457
+ process.kill()
458
+
459
+ # Remove partial output file
460
+ if output_file.exists():
461
+ output_file.unlink()
462
+
463
+ raise
464
+
465
+ # Convert timestamps
466
+ ts1 = time_to_seconds(timestamp1)
467
+ ts2 = time_to_seconds(timestamp2)
468
+ ts3 = time_to_seconds(timestamp3)
469
+
470
+ # Build commands
471
+ commands = [
472
+ (
473
+ [
474
+ ffmpeg_path, '-y', '-i', video_path,
475
+ '-ss', str(ts1), '-t', str(ts2 - ts1),
476
+ '-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
477
+ '-c:a', 'aac', str(first_clip_path)
478
+ ],
479
+ first_clip_path
480
+ ),
481
+ (
482
+ [
483
+ ffmpeg_path, '-y', '-i', video_path,
484
+ '-ss', str(ts2), '-t', str(ts3 - ts2),
485
+ '-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
486
+ '-c:a', 'aac', str(second_clip_path)
487
+ ],
488
+ second_clip_path
489
+ )
490
+ ]
491
+
492
+ try:
493
+ # Process both clips
494
+ for cmd, output_path in commands:
495
+ logger.info("Running command: %s", ' '.join(cmd))
496
+ run_ffmpeg_with_cancel_check(cmd, output_path)
497
+
498
+ return str(first_clip_path), str(second_clip_path)
499
+
500
+ except asyncio.CancelledError:
501
+ # Cleanup both files if either was cancelled
502
+ for path in [first_clip_path, second_clip_path]:
503
+ if path.exists():
504
+ path.unlink()
505
+ raise
506
+
507
+ #################################################
508
+ # Video Processing Loop
509
+ #################################################
510
+
511
+ def process_freeplay(process_id: str, freeplay_video: str) -> float:
512
+ """
513
+ Sample one frame per second from the freeplay clip,
514
+ compute body‐movement metrics and return the average.
515
+ """
516
+ PROGRESS_STORE[process_id].update({"message": "Processing freeplay"})
517
+ cap = cv2.VideoCapture(freeplay_video)
518
+ if not cap.isOpened():
519
+ raise RuntimeError(f"Failed to open freeplay video at {freeplay_video}")
520
+
521
+ # Determine clip duration in seconds
522
+ fps = cap.get(cv2.CAP_PROP_FPS) or 1.0
523
+ total_frames = cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0
524
+ duration = total_frames / fps
525
+
526
+ movements = []
527
+ prev_pose = None
528
+
529
+ for sec in range(int(duration)):
530
+ if PROGRESS_STORE.get(process_id, {}).get('status') == 'cancelled':
531
+ raise asyncio.CancelledError("Processing cancelled")
532
+ print(f"Processing freeplay frame {sec}")
533
+ if PROGRESS_STORE[process_id]["status"] == "cancelled":
534
+ break
535
+
536
+ # Seek by time (ms)
537
+ cap.set(cv2.CAP_PROP_POS_MSEC, sec * 1000)
538
+ ret, frame = cap.read()
539
+ if not ret or frame is None or frame.size == 0:
540
+ logger.warning(f"Freeplay: no frame at {sec}s")
541
+ continue
542
+
543
+ PROGRESS_STORE[process_id].update({
544
+ "current": sec,
545
+ "percent": 10 + int((sec + 1) / duration * 30)
546
+ })
547
+
548
+ try:
549
+ child_roi = detect_child_and_crop(frame)
550
+ pose_kps = process_pose(child_roi)
551
+ mv = calculate_body_movement(pose_kps, prev_pose)
552
+ movements.append(mv)
553
+ prev_pose = pose_kps
554
+ except Exception as e:
555
+ logger.error(f"Freeplay error at {sec}s: {e}", exc_info=True)
556
+
557
+ cap.release()
558
+ return float(np.mean(movements)) if movements else 0.0
559
+
560
+ def process_experiment(process_id: str, experiment_video: str, freeplay_movement: float) -> pd.DataFrame:
561
+ """
562
+ Sample one frame per second from the experiment clip,
563
+ compute all metrics, and return a DataFrame.
564
+ """
565
+ PROGRESS_STORE[process_id].update({"message": "Analyzing experiment"})
566
+ cap = cv2.VideoCapture(experiment_video)
567
+ if not cap.isOpened():
568
+ raise RuntimeError(f"Failed to open experiment video at {experiment_video}")
569
+
570
+ fps = cap.get(cv2.CAP_PROP_FPS) or 1.0
571
+ total_frames = cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0
572
+ duration = total_frames / fps
573
+ PROGRESS_STORE[process_id].update({"total": int(duration)})
574
+
575
+ results = []
576
+ prev_landmarks = None
577
+ prev_pose = None
578
+
579
+ for sec in range(int(duration)):
580
+ if PROGRESS_STORE.get(process_id, {}).get('status') == 'cancelled':
581
+ raise asyncio.CancelledError("Processing cancelled")
582
+ print(f"Processing experiment frame {sec}")
583
+ if PROGRESS_STORE[process_id]["status"] == "cancelled":
584
+ break
585
+
586
+ cap.set(cv2.CAP_PROP_POS_MSEC, sec * 1000)
587
+ ret, frame = cap.read()
588
+ if not ret or frame is None or frame.size == 0:
589
+ logger.warning(f"Experiment: no frame at {sec}s")
590
+ results.append({
591
+ "second": sec,
592
+ "parent_dist": None,
593
+ "stranger_dist": None,
594
+ "face_movement": None,
595
+ "body_movement": None
596
+ })
597
+ continue
598
+
599
+ PROGRESS_STORE[process_id].update({
600
+ "current": sec,
601
+ "percent": 40 + int((sec + 1) / duration * 60)
602
+ })
603
+
604
+ try:
605
+ child_roi = detect_child_and_crop(frame)
606
+ face_score, curr_landmarks = facial_keypoints(child_roi, prev_landmarks)
607
+ pose_kps = process_pose(child_roi)
608
+ body_mv = calculate_body_movement(pose_kps, prev_pose)
609
+ mov_ratio = body_mv / freeplay_movement if freeplay_movement else 0.0
610
+
611
+ parent_dist = calculate_distance_between_objects(frame, "Child", "Adult")
612
+ stranger_dist = calculate_distance_between_objects(frame, "Child", "Stranger")
613
+
614
+ results.append({
615
+ "second": sec,
616
+ "distance_adult": parent_dist,
617
+ "distance_stranger": stranger_dist,
618
+ "facial_movement": face_score,
619
+ "body_movement": mov_ratio
620
+ })
621
+
622
+ prev_landmarks = curr_landmarks
623
+ prev_pose = pose_kps
624
+
625
+ except Exception as e:
626
+ logger.error(f"Experiment error at {sec}s: {e}", exc_info=True)
627
+ # still append a row so CSV timestamps remain aligned
628
+ results.append({
629
+ "second": sec,
630
+ "distance_adult": None,
631
+ "distance_stranger": None,
632
+ "facial_movement": None,
633
+ "body_movement": None
634
+ })
635
+
636
+ cap.release()
637
+ return pd.DataFrame(results)
638
+
639
+ def apply_classes(df, timestamp_start, timestamp_end,
640
+ distance_model_name='distance_classifier.pkl',
641
+ fear_model_name='fear_classifier.pkl',
642
+ freeze_model_name='freeze_classifier.pkl'):
643
+
644
+
645
+ distance_tree_path = Path(__file__).parent / 'models' / distance_model_name
646
+ fear_tree_path = Path(__file__).parent / 'models' / fear_model_name
647
+ freeze_tree_path = Path(__file__).parent / 'models' / freeze_model_name
648
+
649
+ # Load models
650
+ distance_clf = joblib.load(distance_tree_path)
651
+ fear_clf = joblib.load(fear_tree_path)
652
+ freeze_clf = joblib.load(freeze_tree_path)
653
+
654
+ # 1) Initialize outputs
655
+ df['proximity to parent'] = None
656
+ df['proximity to stranger'] = None
657
+ df['fear'] = None
658
+ df['freeze'] = pd.Series([pd.NA] * len(df), dtype="Int64")
659
+
660
+ # 2) Distance → proximity classes
661
+ valid_mask = df[['distance_adult','body_movement','facial_movement']].notnull().all(axis=1)
662
+ preds_parent = distance_clf.predict(df.loc[valid_mask, ['distance_adult']])
663
+ df.loc[valid_mask, 'proximity to parent'] = preds_parent
664
+ df.loc[valid_mask, 'proximity to stranger'] = pd.Series(preds_parent).map({0:2, 1:1, 2:0}).values
665
+
666
+ # 3) Fear classifier
667
+ fear_cols = ['proximity to parent','proximity to stranger','body_movement','facial_movement']
668
+ fear_mask = df[fear_cols].notnull().all(axis=1)
669
+ df.loc[fear_mask, 'fear'] = fear_clf.predict(df.loc[fear_mask, fear_cols])
670
+
671
+ # 4) Build pairwise DataFrame (includes 'second')
672
+ df1 = df.iloc[:-1].reset_index(drop=True).add_suffix('_1')
673
+ df2 = df.iloc[1:].reset_index(drop=True).add_suffix('_2')
674
+ df_pairs = pd.concat([df1, df2], axis=1)
675
+
676
+ # 5) Filter pairs where both fears > 0
677
+ mask = (df_pairs['fear_1'] > 0) & (df_pairs['fear_2'] > 0)
678
+ df_filtered = df_pairs[mask].copy()
679
+ df_filtered['body_movement_avg'] = (df_filtered['body_movement_1'] + df_filtered['body_movement_2']) / 2
680
+
681
+ # 6) Predict freeze and backfill to both seconds
682
+ if not df_filtered.empty:
683
+ df_filtered['freeze'] = freeze_clf.predict(df_filtered[['body_movement_avg']])
684
+ for _, row in df_filtered.iterrows():
685
+ for sec_col in ('second_1', 'second_2'):
686
+ sec = int(row[sec_col])
687
+ idx = df.index[df['second'] == sec][0]
688
+ current = df.at[idx, 'freeze']
689
+ if not (pd.notna(current) and current == 1):
690
+ df.at[idx, 'freeze'] = row['freeze']
691
+
692
+ # 7) Add timestamps column based on timestamp_start and 'second'
693
+ time_format = '%H:%M:%S'
694
+ ts_start = datetime.datetime.strptime(timestamp_start, time_format)
695
+ df['timestamp'] = df['second'].apply(
696
+ lambda x: (ts_start + datetime.timedelta(seconds=int(x))).time().strftime(time_format)
697
+ )
698
+
699
+ # 8) Return only the final columns
700
+ return df[['timestamp', 'second', 'proximity to parent', 'proximity to stranger', 'fear', 'freeze']]
701
+
702
+ async def process_video_async(process_id: str, video_path: Path, session_dir: Path,
703
+ timestamp1: str, timestamp2: str, timestamp3: str, temp_dir: Path):
704
+
705
+ if PROGRESS_STORE.get(process_id, {}).get("started"):
706
+ return
707
+
708
+ # Initialize progress tracking
709
+ PROGRESS_STORE[process_id] = {
710
+ "started": True,
711
+ "status": "processing",
712
+ "percent": 0,
713
+ "message": "Initializing",
714
+ "result": None,
715
+ "error": None
716
+ }
717
+
718
+ # Validate timestamps
719
+ def validate_timestamp(t):
720
+ parts = t.split(':')
721
+ return (len(parts) == 3 and all(p.isdigit() for p in parts))
722
+
723
+ if not all(validate_timestamp(ts) for ts in [timestamp1, timestamp2, timestamp3]):
724
+ raise ValueError("Invalid timestamp format")
725
+
726
+ # Crop video
727
+ PROGRESS_STORE[process_id].update({
728
+ "message": "Cropping video segments",
729
+ "percent": 5
730
+ })
731
+
732
+
733
+ try:
734
+ freeplay_video, experiment_video = await asyncio.to_thread(
735
+ crop_video,
736
+ process_id,
737
+ str(video_path),
738
+ timestamp1,
739
+ timestamp2,
740
+ timestamp3,
741
+ str(temp_dir)
742
+ )
743
+
744
+
745
+ # Process freeplay segment
746
+ PROGRESS_STORE[process_id].update({
747
+ "message": "Analyzing freeplay movement",
748
+ "percent": 10
749
+ })
750
+ freeplay_movement = await asyncio.to_thread(
751
+ process_freeplay,
752
+ process_id,
753
+ freeplay_video
754
+ )
755
+
756
+ # Process experiment segment in a thread
757
+ PROGRESS_STORE[process_id].update({
758
+ "message": "Analyzing experiment",
759
+ "percent": 40
760
+ })
761
+ result_df = await asyncio.to_thread(
762
+ process_experiment,
763
+ process_id,
764
+ experiment_video,
765
+ freeplay_movement
766
+ )
767
+
768
+ final_df = apply_classes(result_df, timestamp2, timestamp3)
769
+
770
+ result_path = session_dir / "analysis.csv"
771
+ final_df.to_csv(result_path, index=False)
772
+ os.sync()
773
+
774
+ PROGRESS_STORE[process_id].update({
775
+ "status": "completed",
776
+ "result": str(result_path),
777
+ "percent": 100,
778
+ "message": "Analysis complete"
779
+ })
780
+
781
+ except Exception as e:
782
+ logger.error(f"Processing error: {str(e)}", exc_info=True)
783
+ PROGRESS_STORE[process_id].update({
784
+ "status": "error",
785
+ "error": str(e),
786
+ "percent": 100
787
+ })
788
+
789
+ finally:
790
+ if video_path.exists():
791
+ video_path.unlink()
792
+
793
+ #################################################
794
+ # API Endpoints
795
+ #################################################
796
+
797
+ @app.post("/api/process-video")
798
+ async def start_processing(
799
+ video: UploadFile = File(...),
800
+ timestamp1: str = Form(...),
801
+ timestamp2: str = Form(...),
802
+ timestamp3: str = Form(...)
803
+ ):
804
+ # 1) Generate IDs & dirs
805
+ process_id = str(uuid.uuid4())
806
+ temp_dir = Path(tempfile.mkdtemp())
807
+ session_dir = OUTPUT_DIR / f"session_{process_id}"
808
+ session_dir.mkdir(exist_ok=True)
809
+
810
+ # 2) Seed progress (so /api/progress can pick it up immediately)
811
+ PROGRESS_STORE[process_id] = {
812
+ "started": False,
813
+ "status": "queued",
814
+ "percent": 0,
815
+ "message": "Queued for processing",
816
+ "result": None,
817
+ "error": None
818
+ }
819
+
820
+ # 3) Save the upload
821
+ video_path = temp_dir / video.filename
822
+ with open(video_path, "wb") as f:
823
+ f.write(await video.read())
824
+
825
+ # 4) Kick off the async worker on the loop directly
826
+ asyncio.create_task(
827
+ process_video_async(
828
+ process_id, video_path, session_dir,
829
+ timestamp1, timestamp2, timestamp3, temp_dir
830
+ )
831
+ )
832
+
833
+ # 5) Return the process_id immediately
834
+ return {"process_id": process_id}
835
+
836
+ @app.get("/api/progress/{process_id}")
837
+ async def progress_stream(process_id: str):
838
+ async def event_generator():
839
+ last = {}
840
+ while True:
841
+ if process_id in PROGRESS_STORE:
842
+ current = PROGRESS_STORE[process_id]
843
+ if current != last:
844
+ last = current.copy() # snapshot instead of alias
845
+ yield f"data: {json.dumps(current)}\n\n"
846
+ if current["status"] in ["completed", "error", "cancelled"]:
847
+ break
848
+ await asyncio.sleep(0.5)
849
+
850
+ return StreamingResponse(
851
+ event_generator(),
852
+ media_type="text/event-stream",
853
+ headers={
854
+ "Cache-Control": "no-cache",
855
+ "Connection": "keep-alive" # ensure the stream stays open
856
+ }
857
+ )
858
+
859
+ @app.get("/api/results/{process_id}")
860
+ async def results(process_id: str):
861
+ if process_id not in PROGRESS_STORE:
862
+ raise HTTPException(404, detail="Process ID not found")
863
+
864
+ status = PROGRESS_STORE[process_id]
865
+
866
+ if status["status"] == "completed":
867
+ csv_path = Path(status["result"])
868
+ try:
869
+ # Validate file exists and is readable
870
+ if not csv_path.exists() or csv_path.stat().st_size == 0:
871
+ raise FileNotFoundError("Result file missing or empty")
872
+
873
+ return FileResponse(
874
+ csv_path,
875
+ media_type="text/csv",
876
+ filename="stranger_danger_analysis.csv",
877
+ headers={"X-Analysis-Complete": "true"}
878
+ )
879
+ except Exception as e:
880
+ logger.error(f"Results delivery failed: {str(e)}")
881
+ raise HTTPException(500, detail="Results generation failed")
882
+
883
+ raise HTTPException(425, detail="Analysis not complete yet")
884
+
885
+ @app.post("/api/cancel-analysis")
886
+ async def cancel_analysis(process_id: str = Form(...)):
887
+ if process_id in PROGRESS_STORE:
888
+ PROGRESS_STORE[process_id].update({"status": "cancelled", "message": "Cancelled by user"})
889
+ return {"status": "cancelled"}
890
+
891
+ @app.post("/api/delete-video")
892
+ async def delete_video(process_id: str = Form(...)):
893
+ if process_id in PROGRESS_STORE:
894
+ PROGRESS_STORE.pop(process_id, None)
895
+ return {"status": "deleted"}
896
+ raise HTTPException(404, detail="Video not found")
897
+
898
+ @app.get("/{full_path:path}")
899
+ async def serve_frontend(full_path: str):
900
+ if full_path.startswith(("api/", "static/")):
901
+ raise HTTPException(status_code=404)
902
+ frontend = Path("frontend/index.html")
903
+ if not frontend.exists():
904
+ raise HTTPException(status_code=404, detail="Frontend not found")
905
+ return FileResponse(frontend)
906
+
907
+ if __name__ == "__main__":
908
+ import uvicorn
909
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
910
+
911
+
backend/midas_utils/__init__.py ADDED
File without changes
backend/midas_utils/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (167 Bytes). View file
 
backend/midas_utils/__pycache__/transforms.cpython-310.pyc ADDED
Binary file (1.91 kB). View file
 
backend/midas_utils/fresh_model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:70d6b9c891758c67f974a6097fb0c608c7ee67fb81ac3e5588847d5596d56fca
3
+ size 85761505
backend/midas_utils/model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:70d6b9c891758c67f974a6097fb0c608c7ee67fb81ac3e5588847d5596d56fca
3
+ size 85761505
backend/midas_utils/transforms.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+ import torch
4
+ from torchvision import transforms
5
+
6
+ class Resize(object):
7
+ def __init__(self, size):
8
+ self.size = size
9
+
10
+ def __call__(self, image):
11
+ image = cv2.resize(image, (self.size, self.size))
12
+ return image
13
+
14
+ class NormalizeImage(object):
15
+ def __init__(self, mean, std):
16
+ self.mean = mean
17
+ self.std = std
18
+
19
+ def __call__(self, image):
20
+ image = image.astype(np.float32) / 255.0
21
+ image -= np.array(self.mean)
22
+ image /= np.array(self.std)
23
+ return image
24
+
25
+ class PrepareForNet(object):
26
+ def __call__(self, image):
27
+ image = torch.from_numpy(image)
28
+ if len(image.shape) == 3:
29
+ image = image.permute(2, 0, 1)
30
+ image = image.unsqueeze(0)
31
+ return image
32
+
33
+ class Compose:
34
+ def __init__(self, transforms):
35
+ self.transforms = transforms
36
+
37
+ def __call__(self, img):
38
+ for t in self.transforms:
39
+ img = t(img)
40
+ return img
backend/models/.DS_Store ADDED
Binary file (6.15 kB). View file
 
backend/models/distance_classifier.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f5e48f5a4ec6ad18315c3a4c3a97cd76a506b35147008db0ca420056b6767a5e
3
+ size 2241
backend/models/fear_classifier.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ea252c3a845a28cac79a1b1ed944f4929a3e510286a772b3dccf6ba8412697c1
3
+ size 4273
backend/models/freeze_classifier.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:75491fb862b4c0bfdc79c214bdf5bdaa32622c5908cc2215e7a467754923bfe6
3
+ size 3129
backend/models/yolo_retrained_model.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f09573aee77e183bad25d85a07f58be838d9e02bfbfcb0fdefb73bd59dddc117
3
+ size 52045563
backend/models/yolov8n-pose.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c6fa93dd1ee4a2c18c900a45c1d864a1c6f7aba75d84f91648a30b7fb641d212
3
+ size 6832633
ffmpeg ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e7e7fb30477f717e6f55f9180a70386c62677ef8a4d4d1a5d948f4098aa3eb99
3
+ size 79826272
frontend/.DS_Store ADDED
Binary file (6.15 kB). View file
 
frontend/index.html ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Experiment Auto-Labeler</title>
7
+ <base href="/">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
9
+ <link rel="stylesheet" href="/static/style.css">
10
+ </head>
11
+ <body>
12
+ <div class="container">
13
+ <!-- Initial Screen -->
14
+ <div class="card" id="initialScreen">
15
+ <h1>Stranger Danger Auto-Labeling</h1>
16
+ <div class="option-grid">
17
+ <div class="option-card" onclick="showUploadScreen('sharepoint')">
18
+ <i class="fab fa-microsoft"></i>
19
+ <h3>SharePoint</h3>
20
+ <p>Access videos from SharePoint</p>
21
+ </div>
22
+ <div class="option-card" onclick="showUploadScreen('local')">
23
+ <i class="fas fa-upload"></i>
24
+ <h3>Local Upload</h3>
25
+ <p>Upload from your device</p>
26
+ </div>
27
+ </div>
28
+ </div>
29
+
30
+ <!-- SharePoint Credentials Screen -->
31
+ <div class="card hidden" id="sharepointCredScreen">
32
+ <h2>SharePoint Connection</h2>
33
+ <form id="spCredForm" onsubmit="handleSpCredSubmit(event)">
34
+ <div class="form-group">
35
+ <label>Site URL</label>
36
+ <input type="url" id="spSiteUrl" required placeholder="https://yourdomain.sharepoint.com/sites/yoursite">
37
+ </div>
38
+ <div class="form-group">
39
+ <label>Client ID</label>
40
+ <input type="text" id="spClientId" required placeholder="a1b2c3d4-e5f6-7g8h-9i0j-k1l2m3n4o5p6">
41
+ </div>
42
+ <div class="form-group">
43
+ <label>Client Secret</label>
44
+ <input type="password" id="spClientSecret" required placeholder="ABC123~abcdefghijklmnopqrstuvwxyz">
45
+ </div>
46
+ <div class="form-group">
47
+ <label>Document Library</label>
48
+ <input type="text" id="spDocLibrary" value="Documents" required>
49
+ </div>
50
+ <button type="submit" class="btn">
51
+ <i class="fas fa-check"></i> Connect
52
+ </button>
53
+ <button type="button" class="btn secondary" onclick="showScreen('initialScreen')">
54
+ <i class="fas fa-arrow-left"></i> Back
55
+ </button>
56
+ </form>
57
+ </div>
58
+
59
+ <!-- SharePoint File Selection -->
60
+ <div class="card hidden" id="sharepointFileScreen">
61
+ <h2>Select SharePoint File</h2>
62
+ <div id="spFileList"></div>
63
+ <button class="btn secondary" onclick="showScreen('sharepointCredScreen')">
64
+ <i class="fas fa-arrow-left"></i> Back
65
+ </button>
66
+ </div>
67
+
68
+ <!-- Local Upload Screen -->
69
+ <div class="card hidden" id="localUploadScreen">
70
+ <h2>Upload Video</h2>
71
+ <div class="upload-area" id="dropZone">
72
+ <i class="fas fa-cloud-upload-alt"></i>
73
+ <p>Drag & drop or click to upload</p>
74
+ <input type="file" id="videoInput" hidden accept="video/*">
75
+ </div>
76
+ <div class="preview-container">
77
+ <video id="videoPreview" class="hidden" controls></video>
78
+ </div>
79
+
80
+ <!-- Add timestamp inputs -->
81
+ <div class="timestamp-group">
82
+ <div class="form-group">
83
+ <label>Start Time (HH:MM:SS)</label>
84
+ <input type="text" id="timestamp1" required
85
+ pattern="^([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$"
86
+ placeholder="00:00:00">
87
+ </div>
88
+ <div class="form-group">
89
+ <label>Transition Time (HH:MM:SS)</label>
90
+ <input type="text" id="timestamp2" required
91
+ pattern="^([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$"
92
+ placeholder="00:00:00">
93
+ </div>
94
+ <div class="form-group">
95
+ <label>End Time (HH:MM:SS)</label>
96
+ <input type="text" id="timestamp3" required
97
+ pattern="^([0-1][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$"
98
+ placeholder="00:00:00">
99
+ </div>
100
+ </div>
101
+
102
+ <button class="btn" id="analyzeBtn" disabled>
103
+ <i class="fas fa-play"></i> Start Analysis
104
+ </button>
105
+ <button class="btn secondary" onclick="showScreen('initialScreen')">
106
+ <i class="fas fa-arrow-left"></i> Back
107
+ </button>
108
+ </div>
109
+
110
+ <!-- Progress Screen -->
111
+ <div class="card hidden" id="progressScreen">
112
+ <h2>Analyzing Video</h2>
113
+ <div class="progress-container">
114
+ <div class="progress-bar" id="progressBar"></div>
115
+ <div id="frameCounter"></div>
116
+ </div>
117
+ <p id="progressMessage">Initializing analysis... Do not cancel</p>
118
+ <div class="button-group">
119
+ <button class="btn danger" id="cancelBtn" onclick="cancelAnalysis()">
120
+ <i class="fas fa-stop-circle"></i> Cancel Analysis
121
+ </button>
122
+ </div>
123
+ </div>
124
+
125
+ <!-- Results Screen -->
126
+ <div class="card hidden" id="resultsScreen">
127
+ <h2>Analysis Complete!</h2>
128
+ <div class="result-badge">
129
+ <i class="fas fa-check-circle"></i>
130
+ </div>
131
+ <button class="btn" id="downloadBtn">
132
+ <i class="fas fa-download"></i> Download Report
133
+ </button>
134
+ <button class="btn secondary" id="newAnalysisBtn">
135
+ <i class="fas fa-redo"></i> New Analysis
136
+ </button>
137
+ </div>
138
+ </div>
139
+ <script src="/static/script.js"></script>
140
+ </body>
141
+ </html>
frontend/static/script.js ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ==================== Constants & Global State ====================
2
+ const API_BASE_URL = window.location.origin; // Base URL for API calls, using current origin
3
+
4
+ let currentFile = null; // Holds the currently selected video file (Blob or File)
5
+ let analysisAbortController = null; // Controller to abort video analysis requests
6
+ let spCredentials = {}; // Stores SharePoint credentials after connection
7
+ let isSharePointFile = false; // Flag indicating if the file came from SharePoint
8
+ let progressSource = null; // EventSource for server-sent events during processing
9
+
10
+ // ==================== Initialization ====================
11
+
12
+ document.addEventListener('DOMContentLoaded', () => {
13
+ initApp(); // Kick off app setup
14
+ });
15
+
16
+ function initApp() {
17
+ initEventListeners(); // Attach all UI event handlers
18
+ showScreen('initialScreen'); // Display the upload/timestamp screen
19
+ }
20
+
21
+ function initEventListeners() {
22
+ // File upload via click
23
+ document.getElementById('dropZone').addEventListener('click', () => {
24
+ document.getElementById('videoInput').click(); // Trigger hidden file input
25
+ });
26
+
27
+ // File input change handler
28
+ document.getElementById('videoInput').addEventListener('change', handleFileSelect);
29
+
30
+ // Drag-and-drop handlers
31
+ const dropZone = document.getElementById('dropZone');
32
+ dropZone.addEventListener('dragover', handleDragOver); // Highlight zone on drag over
33
+ dropZone.addEventListener('drop', handleDrop); // Handle file drop
34
+ dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover')); // Remove highlight
35
+
36
+ // Navigation buttons to switch screens
37
+ document.querySelectorAll('[data-screen]').forEach(btn => {
38
+ btn.addEventListener('click', () => {
39
+ if (analysisAbortController) {
40
+ // If analysis in flight, cancel then switch
41
+ cancelAnalysis().finally(() => showScreen(btn.dataset.screen));
42
+ } else {
43
+ showScreen(btn.dataset.screen);
44
+ }
45
+ });
46
+ });
47
+
48
+ // "New Analysis" button on results screen
49
+ document.querySelector('#resultsScreen .btn.secondary').addEventListener('click', handleNewAnalysis);
50
+
51
+ // Analysis control buttons
52
+ document.getElementById('analyzeBtn').addEventListener('click', startAnalysis); // Start processing
53
+ document.getElementById('cancelBtn').addEventListener('click', cancelAnalysis); // Cancel processing
54
+ document.getElementById('downloadBtn').addEventListener('click', () => {
55
+ // Download handled dynamically in setupDownload()
56
+ });
57
+
58
+ // Timestamp input validation handlers
59
+ document.getElementById('timestamp1').addEventListener('input', validateTimestamps);
60
+ document.getElementById('timestamp2').addEventListener('input', validateTimestamps);
61
+ document.getElementById('timestamp3').addEventListener('input', validateTimestamps);
62
+ }
63
+
64
+ // ==================== High-Level Workflows ====================
65
+
66
+ // Start a brand new analysis (from results screen)
67
+ async function handleNewAnalysis() {
68
+ try {
69
+ await cancelAnalysis(); // Abort any running job
70
+ resetApp(); // Clear form and state
71
+ resetAnalyzeButton(); // Restore Analyze button
72
+ showScreen('initialScreen'); // Go back to upload
73
+ } catch (error) {
74
+ showError(`Failed to start new analysis: ${error.message}`); // Show error
75
+ }
76
+ }
77
+
78
+ // Handle SharePoint credentials submission and file listing
79
+ async function handleSpCredSubmit(event) {
80
+ event.preventDefault();
81
+
82
+ const submitBtn = event.target.querySelector('button[type="submit"]');
83
+ const originalText = submitBtn.innerHTML;
84
+ submitBtn.disabled = true; // Prevent double submits
85
+ submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Connecting...'; // Show spinner
86
+
87
+ // Collect credentials from form
88
+ spCredentials = {
89
+ siteUrl: document.getElementById('spSiteUrl').value.trim(),
90
+ clientId: document.getElementById('spClientId').value.trim(),
91
+ clientSecret: document.getElementById('spClientSecret').value.trim(),
92
+ docLibrary: document.getElementById('spDocLibrary').value.trim()
93
+ };
94
+
95
+ try {
96
+ const response = await fetch(`${API_BASE_URL}/api/sharepoint/files`, {
97
+ method: 'POST',
98
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
99
+ body: new URLSearchParams({
100
+ ...spCredentials,
101
+ doc_library: spCredentials.docLibrary
102
+ })
103
+ });
104
+
105
+ if (!response.ok) {
106
+ const errorData = await response.json().catch(() => ({}));
107
+ throw new Error(errorData.detail || response.statusText);
108
+ }
109
+
110
+ const files = await response.json(); // Array of SharePoint files
111
+ renderSpFileList(files); // Populate file list UI
112
+ showScreen('sharepointFileScreen'); // Switch to file selection
113
+ } catch (error) {
114
+ showError(`SharePoint connection failed: ${error.message}`);
115
+ } finally {
116
+ submitBtn.disabled = false; // Restore button
117
+ submitBtn.innerHTML = originalText;
118
+ }
119
+ }
120
+
121
+ // Render list of SharePoint files with Select buttons
122
+ function renderSpFileList(files) {
123
+ const fileList = document.getElementById('spFileList');
124
+ if (!fileList) return;
125
+
126
+ fileList.innerHTML = files.map(file => `
127
+ <div class="sp-file-item">
128
+ <span>${file.name}</span>
129
+ <button class="btn" onclick="handleSpFile('${file.id}')">
130
+ <i class="fas fa-play"></i> Select
131
+ </button>
132
+ </div>
133
+ `).join('');
134
+ }
135
+
136
+ // Handle selecting and downloading a file from SharePoint
137
+ async function handleSpFile(fileId) {
138
+ const selectBtn = event.target;
139
+ const originalText = selectBtn.innerHTML;
140
+ selectBtn.disabled = true;
141
+ selectBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Loading...';
142
+
143
+ try {
144
+ const formData = new URLSearchParams({
145
+ ...spCredentials,
146
+ file_id: fileId
147
+ });
148
+
149
+ const response = await fetch(`${API_BASE_URL}/api/sharepoint/download`, {
150
+ method: 'POST',
151
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
152
+ body: formData
153
+ });
154
+
155
+ if (!response.ok) {
156
+ const errorData = await response.json().catch(() => ({}));
157
+ throw new Error(errorData.detail || response.statusText);
158
+ }
159
+
160
+ currentFile = await response.blob(); // Store the downloaded blob
161
+ isSharePointFile = true; // Mark as SharePoint source
162
+ await startAnalysis(); // Begin processing
163
+ } catch (error) {
164
+ showError(`File download failed: ${error.message}`);
165
+ } finally {
166
+ selectBtn.disabled = false; // Restore button
167
+ selectBtn.innerHTML = originalText;
168
+ }
169
+ }
170
+
171
+ // Kick off video analysis by sending file and timestamps to backend
172
+ async function startAnalysis() {
173
+ const analyzeBtn = document.getElementById('analyzeBtn');
174
+ analyzeBtn.disabled = true; // Prevent re-click
175
+ analyzeBtn.onclick = null;
176
+ analyzeBtn.innerText = 'Analyzing…'; // Update label
177
+
178
+ if (!currentFile) {
179
+ showError('Please select a file first!');
180
+ resetAnalyzeButton();
181
+ return;
182
+ }
183
+
184
+ const t1 = document.getElementById('timestamp1').value;
185
+ const t2 = document.getElementById('timestamp2').value;
186
+ const t3 = document.getElementById('timestamp3').value;
187
+ if (!validateTimeOrder(t1, t2, t3)) {
188
+ showError('Timestamps must be in ascending order');
189
+ resetAnalyzeButton();
190
+ return;
191
+ }
192
+
193
+ showScreen('progressScreen'); // Show progress UI
194
+ analysisAbortController = new AbortController(); // New controller
195
+
196
+ try {
197
+ const formData = new FormData();
198
+ formData.append('video', currentFile);
199
+ formData.append('timestamp1', t1);
200
+ formData.append('timestamp2', t2);
201
+ formData.append('timestamp3', t3);
202
+
203
+ const response = await fetch(`${API_BASE_URL}/api/process-video`, {
204
+ method: 'POST',
205
+ body: formData,
206
+ signal: analysisAbortController.signal
207
+ });
208
+
209
+ if (!response.ok) {
210
+ const errorData = await response.json().catch(() => ({}));
211
+ throw new Error(errorData.detail || response.statusText);
212
+ }
213
+
214
+ const { process_id } = await response.json();
215
+ setupProgressTracker(process_id);
216
+
217
+ } catch (error) {
218
+ if (error.name !== 'AbortError') {
219
+ showError(`Analysis failed: ${error.message}`);
220
+ showScreen('initialScreen');
221
+ }
222
+ }
223
+ }
224
+
225
+ // ==================== File Upload/Selection Handlers ====================
226
+
227
+ function handleFileSelect(e) {
228
+ const file = e.target.files[0];
229
+ if (file) handleFile(file);
230
+ }
231
+
232
+ function handleDragOver(e) {
233
+ e.preventDefault();
234
+ e.stopPropagation();
235
+ e.currentTarget.classList.add('dragover');
236
+ }
237
+
238
+ function handleDrop(e) {
239
+ e.preventDefault();
240
+ e.stopPropagation();
241
+ e.currentTarget.classList.remove('dragover');
242
+ const file = e.dataTransfer.files[0];
243
+ if (file) handleFile(file);
244
+ }
245
+
246
+ function handleFile(file) {
247
+ if (!file || !file.type.startsWith('video/')) {
248
+ showError('Please upload a valid video file (MP4, MOV, or AVI)');
249
+ return;
250
+ }
251
+
252
+ currentFile = file;
253
+ isSharePointFile = false;
254
+
255
+ const preview = document.getElementById('videoPreview');
256
+ const analyzeBtn = document.getElementById('analyzeBtn');
257
+
258
+ if (preview.src) URL.revokeObjectURL(preview.src);
259
+
260
+ preview.src = URL.createObjectURL(file);
261
+ preview.classList.remove('hidden');
262
+ analyzeBtn.disabled = false;
263
+
264
+ document.getElementById('timestamp1').value = '';
265
+ document.getElementById('timestamp2').value = '';
266
+ document.getElementById('timestamp3').value = '';
267
+ validateTimestamps();
268
+ }
269
+
270
+ function showUploadScreen(type) {
271
+ if (type === 'sharepoint') {
272
+ showScreen('sharepointCredScreen');
273
+ } else {
274
+ showScreen('localUploadScreen');
275
+ }
276
+ }
277
+
278
+ // ==================== Progress Tracking ====================
279
+
280
+ function setupProgressTracker(processId) {
281
+ if (progressSource) progressSource.close();
282
+
283
+ progressSource = new EventSource(`${API_BASE_URL}/api/progress/${processId}`);
284
+
285
+ progressSource.onmessage = (event) => {
286
+ try {
287
+ const data = JSON.parse(event.data);
288
+
289
+ if (data.status === 'completed') {
290
+ handleAnalysisComplete(processId);
291
+ progressSource.close();
292
+ } else if (data.status === 'error') {
293
+ showError(data.error || 'Analysis failed');
294
+ progressSource.close();
295
+ showScreen('initialScreen');
296
+ } else {
297
+ updateProgressUI(data);
298
+ }
299
+ } catch (error) {
300
+ console.error('Error parsing progress:', error);
301
+ }
302
+ };
303
+
304
+ progressSource.onerror = () => {
305
+ console.log('SSE error - attempting reconnect');
306
+ setTimeout(() => setupProgressTracker(processId), 2000);
307
+ };
308
+ }
309
+
310
+ function updateProgressUI(progress) {
311
+ const progressBar = document.getElementById('progressBar');
312
+ const progressMessage = document.getElementById('progressMessage');
313
+
314
+ progressBar.style.width = `${progress.percent}%`;
315
+ progressMessage.textContent = progress.message;
316
+
317
+ if (progress.current && progress.total) {
318
+ document.getElementById('frameCounter').textContent = `${progress.current}/${progress.total} seconds processed`;
319
+ }
320
+ }
321
+
322
+ async function handleAnalysisComplete(processId) {
323
+ try {
324
+ const response = await fetch(`${API_BASE_URL}/api/results/${processId}`);
325
+ const blob = await response.blob();
326
+ setupDownload(blob);
327
+ showScreen('resultsScreen');
328
+ } catch (error) {
329
+ showError('Failed to retrieve results');
330
+ }
331
+ }
332
+
333
+ // ==================== Utilities ====================
334
+
335
+ function showScreen(screenId) {
336
+ document.querySelectorAll('.card').forEach(el => el.classList.add('hidden'));
337
+ const targetScreen = document.getElementById(screenId);
338
+ if (targetScreen) {
339
+ targetScreen.classList.remove('hidden');
340
+ window.scrollTo(0, 0);
341
+ } else {
342
+ console.error(`Screen with ID ${screenId} not found`);
343
+ }
344
+ }
345
+
346
+ function showError(message) {
347
+ const errorDiv = document.createElement('div');
348
+ errorDiv.className = 'error-message';
349
+ errorDiv.innerHTML = `<i class="fas fa-exclamation-circle"></i><span>${message}</span>`;
350
+ document.body.prepend(errorDiv);
351
+ setTimeout(() => { errorDiv.classList.add('fade-out'); setTimeout(() => errorDiv.remove(), 500); }, 5000);
352
+ }
353
+
354
+ function validateTimestamps() {
355
+ const t1 = document.getElementById('timestamp1');
356
+ const t2 = document.getElementById('timestamp2');
357
+ const t3 = document.getElementById('timestamp3');
358
+ const analyzeBtn = document.getElementById('analyzeBtn');
359
+ const isValid = t1.checkValidity() && t2.checkValidity() && t3.checkValidity() && t1.value !== '' && t2.value !== '' && t3.value !== '';
360
+ analyzeBtn.disabled = !isValid;
361
+ }
362
+
363
+ function validateTimeOrder(t1, t2, t3) {
364
+ const toSeconds = t => { const [h, m, s] = t.split(':').map(Number); return h*3600 + m*60 + s; };
365
+ return toSeconds(t1) < toSeconds(t2) && toSeconds(t2) < toSeconds(t3);
366
+ }
367
+
368
+ function resetAnalyzeButton() {
369
+ const btn = document.getElementById('analyzeBtn');
370
+ btn.disabled = false;
371
+ btn.innerText = 'Start Analysis';
372
+ btn.onclick = startAnalysis;
373
+ }
374
+
375
+ function resetApp() {
376
+ const preview = document.getElementById('videoPreview');
377
+ if (preview.src) URL.revokeObjectURL(preview.src);
378
+ preview.src = '';
379
+ preview.classList.add('hidden');
380
+ document.getElementById('videoInput').value = '';
381
+ const progressBar = document.getElementById('progressBar'); if (progressBar) progressBar.style.width = '0%';
382
+ const progressMessage = document.getElementById('progressMessage'); if (progressMessage) progressMessage.textContent = '';
383
+ const spForm = document.getElementById('spCredForm'); if (spForm) spForm.reset();
384
+ if (progressSource) { progressSource.close(); progressSource = null; }
385
+ currentFile = null;
386
+ isSharePointFile = false;
387
+ spCredentials = {};
388
+ }
389
+
390
+ async function cancelAnalysis() {
391
+ try {
392
+ if (progressSource) {
393
+ progressSource.close();
394
+ progressSource = null;
395
+ }
396
+ if (!analysisAbortController) return;
397
+ const progressMessage = document.getElementById('progressMessage'); if (progressMessage) progressMessage.textContent = "Cancelling analysis...";
398
+ analysisAbortController.abort();
399
+ await fetch(`${API_BASE_URL}/api/cancel-analysis`, { method: 'POST' });
400
+ } catch (error) {
401
+ console.error('Cancellation error:', error);
402
+ throw error;
403
+ } finally {
404
+ analysisAbortController = null;
405
+ }
406
+ }
407
+
408
+ function setupDownload(blob) {
409
+ const url = URL.createObjectURL(blob);
410
+ const downloadBtn = document.getElementById('downloadBtn');
411
+ downloadBtn.onclick = null;
412
+ downloadBtn.onclick = () => {
413
+ const a = document.createElement('a');
414
+ a.href = url;
415
+ a.download = `stranger_danger_analysis_${new Date().toISOString().slice(0,10)}.csv`;
416
+ document.body.appendChild(a);
417
+ a.click();
418
+ setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100);
419
+ };
420
+ }
421
+
422
+ // ==================== Global Exports ====================
423
+
424
+ window.showUploadScreen = showUploadScreen;
425
+ window.handleSpCredSubmit = handleSpCredSubmit;
426
+ window.handleSpFile = handleSpFile;
427
+ window.startAnalysis = startAnalysis;
428
+ window.cancelAnalysis = cancelAnalysis;
429
+ window.resetApp = resetApp;
frontend/static/style.css ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary: #2A2F4F;
3
+ --secondary: #917FB3;
4
+ --background: #FDE2F3;
5
+ --text: #2A2F4F;
6
+ --success: #4CAF50;
7
+ --danger: #dc3545;
8
+ }
9
+
10
+ * {
11
+ box-sizing: border-box;
12
+ margin: 0;
13
+ padding: 0;
14
+ }
15
+
16
+ body {
17
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
18
+ background: var(--background);
19
+ color: var(--text);
20
+ min-height: 100vh;
21
+ line-height: 1.6;
22
+ padding: 0;
23
+ margin: 0;
24
+ }
25
+
26
+ .container {
27
+ max-width: 1200px;
28
+ margin: 0 auto;
29
+ padding: 2rem;
30
+ min-height: 100vh;
31
+ display: flex;
32
+ flex-direction: column;
33
+ justify-content: center;
34
+ }
35
+
36
+ .card {
37
+ background: white;
38
+ border-radius: 1rem;
39
+ padding: 2rem;
40
+ box-shadow: 0 4px 20px rgba(0,0,0,0.1);
41
+ margin: 1rem auto;
42
+ width: 100%;
43
+ max-width: 800px;
44
+ }
45
+
46
+ h1, h2, h3 {
47
+ color: var(--primary);
48
+ margin-bottom: 1rem;
49
+ }
50
+
51
+ .option-grid {
52
+ display: grid;
53
+ gap: 1.5rem;
54
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
55
+ margin: 2rem 0;
56
+ }
57
+
58
+ .option-card {
59
+ padding: 2rem;
60
+ border: 2px solid var(--primary);
61
+ border-radius: 1rem;
62
+ cursor: pointer;
63
+ transition: transform 0.2s;
64
+ }
65
+
66
+ .option-card:hover {
67
+ transform: translateY(-5px);
68
+ }
69
+
70
+ .btn {
71
+ background: var(--primary);
72
+ color: white;
73
+ border: none;
74
+ padding: 1rem 2rem;
75
+ border-radius: 0.5rem;
76
+ cursor: pointer;
77
+ font-size: 1rem;
78
+ transition: transform 0.2s;
79
+ display: inline-flex;
80
+ align-items: center;
81
+ gap: 0.5rem;
82
+ }
83
+
84
+ .btn:hover {
85
+ transform: translateY(-2px);
86
+ }
87
+
88
+ .secondary {
89
+ background: var(--secondary);
90
+ }
91
+
92
+ .upload-area {
93
+ border: 2px dashed var(--primary);
94
+ border-radius: 1rem;
95
+ padding: 3rem 2rem;
96
+ margin: 2rem 0;
97
+ cursor: pointer;
98
+ }
99
+
100
+ .preview-container {
101
+ width: 100%;
102
+ max-width: 600px;
103
+ margin: 1rem auto;
104
+ }
105
+
106
+ #videoPreview {
107
+ width: 100%;
108
+ max-width: 100%;
109
+ border-radius: 0.5rem;
110
+ display: block;
111
+ margin: 1rem 0;
112
+ }
113
+
114
+ .progress-container {
115
+ width: 100%;
116
+ margin: 2rem 0;
117
+ }
118
+
119
+ .progress-bar {
120
+ height: 20px;
121
+ background: var(--primary);
122
+ border-radius: 10px;
123
+ transition: width 0.3s ease;
124
+ width: 0%;
125
+ }
126
+
127
+ .hidden {
128
+ display: none !important;
129
+ }
130
+
131
+ .result-badge {
132
+ font-size: 4rem;
133
+ color: var(--primary);
134
+ margin: 2rem 0;
135
+ }
136
+
137
+ .form-group {
138
+ margin: 1rem 0;
139
+ }
140
+
141
+ .form-group label {
142
+ display: block;
143
+ margin-bottom: 0.5rem;
144
+ font-weight: 500;
145
+ }
146
+
147
+ .form-group input {
148
+ width: 100%;
149
+ padding: 0.8rem;
150
+ border: 1px solid #ddd;
151
+ border-radius: 0.5rem;
152
+ font-size: 1rem;
153
+ }
154
+
155
+ .sp-file-item {
156
+ padding: 1rem;
157
+ margin: 0.5rem 0;
158
+ border: 1px solid #ddd;
159
+ border-radius: 0.5rem;
160
+ display: flex;
161
+ justify-content: space-between;
162
+ align-items: center;
163
+ background: #fff;
164
+ }
165
+
166
+ .sp-file-item:hover {
167
+ background: #f8f9fa;
168
+ }
169
+
170
+ #analyzeBtn {
171
+ margin-top: 1rem;
172
+ }
173
+
174
+ #frameCounter {
175
+ text-align: center;
176
+ margin-top: 0.5rem;
177
+ font-size: 0.9em;
178
+ color: #666;
179
+ }
180
+
181
+ #cancelBtn {
182
+ margin-top: 1rem;
183
+ background: #dc3545;
184
+ }
185
+
186
+ #cancelBtn:hover {
187
+ background: #c82333;
188
+ transform: translateY(-2px);
189
+ }
190
+
191
+ .btn.danger {
192
+ background: #dc3545;
193
+ color: white;
194
+ }
195
+
196
+ .btn.danger:hover {
197
+ background: #c82333;
198
+ transform: translateY(-2px);
199
+ }
200
+
201
+ .button-group {
202
+ display: flex;
203
+ gap: 1rem;
204
+ justify-content: center;
205
+ margin-top: 1.5rem;
206
+ }
207
+
208
+ /* Add to your existing CSS */
209
+ #progressBar {
210
+ height: 20px;
211
+ background: var(--primary);
212
+ border-radius: 10px;
213
+ transition: width 0.3s ease;
214
+ width: 0%;
215
+ }
216
+
217
+ #frameCounter {
218
+ display: block;
219
+ text-align: center;
220
+ margin-top: 0.5rem;
221
+ color: var(--text);
222
+ font-size: 0.9em;
223
+ }
224
+
225
+ #newAnalysisBtn {
226
+ margin-top: 1rem;
227
+ }
228
+
229
+ /* Add to style.css */
230
+ .timestamp-group {
231
+ display: grid;
232
+ gap: 1rem;
233
+ margin: 1.5rem 0;
234
+ }
235
+
236
+ .timestamp-group .form-group {
237
+ margin: 0;
238
+ }
239
+
240
+ input[type="text"] {
241
+ width: 100%;
242
+ padding: 0.8rem;
243
+ border: 1px solid #ddd;
244
+ border-radius: 4px;
245
+ font-size: 1rem;
246
+ }
247
+
248
+ input:invalid {
249
+ border-color: #ff4444;
250
+ box-shadow: 0 0 3px #ff4444;
251
+ }
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.68.0
2
+ uvicorn>=0.15.0
3
+ opencv-python-headless>=4.5.3
4
+ ultralytics>=8.0.0
5
+ mediapipe>=0.8.9.1
6
+ pandas>=1.3.0
7
+ numpy>=1.21.0
8
+ python-multipart>=0.0.5
9
+ aiohttp>=3.7.4
10
+ office365-rest-python-client>=2.3.12
11
+ ffmpeg>=0.2.0
12
+ joblib>=1.4.2
13
+ scikit-learn>=1.6.1