thenuka1999 commited on
Commit
20acb83
·
1 Parent(s): 45b923d

Add initial project files including Dockerfile, main application logic, model checkpoint, and requirements

Browse files
Files changed (5) hide show
  1. .gitignore +2 -0
  2. Dockerfile +15 -0
  3. elephant_stress_r2plus1d18.pt +3 -0
  4. main.py +143 -0
  5. requirements.txt +8 -0
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ __pycache__/
2
+ *.pyc
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ RUN apt-get update \
4
+ && apt-get install -y --no-install-recommends ffmpeg \
5
+ && rm -rf /var/lib/apt/lists/*
6
+
7
+ WORKDIR /app
8
+
9
+ COPY requirements.txt .
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ COPY . .
13
+
14
+ ENV PORT=7860
15
+ CMD ["bash", "-lc", "uvicorn main:app --host 0.0.0.0 --port ${PORT}"]
elephant_stress_r2plus1d18.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a4db9df704d55a96b31c538ed6e2c54bb1fff38eebfaaac6596998abc55c6c7a
3
+ size 125385517
main.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from tempfile import NamedTemporaryFile
3
+ from typing import Dict, List
4
+
5
+ import numpy as np
6
+ import torch
7
+ import torch.nn as nn
8
+ import torchvision.transforms as T
9
+ from decord import VideoReader, cpu
10
+ from fastapi import FastAPI, File, HTTPException, UploadFile
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from torchvision.models.video import R2Plus1D_18_Weights, r2plus1d_18
13
+
14
+ app = FastAPI(title="Elephant Stress API")
15
+
16
+ app.add_middleware(
17
+ CORSMiddleware,
18
+ allow_origins=["*"],
19
+ allow_credentials=False,
20
+ allow_methods=["*"],
21
+ allow_headers=["*"],
22
+ )
23
+
24
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
25
+ CKPT_PATH = "elephant_stress_r2plus1d18.pt"
26
+ CLIP_LEN = 16
27
+ NUM_CLASSES = 2
28
+
29
+ EVAL_FRAME_TRANSFORM = T.Compose(
30
+ [
31
+ T.ToPILImage(),
32
+ T.Resize((128, 171)),
33
+ T.CenterCrop((112, 112)),
34
+ T.ToTensor(),
35
+ ]
36
+ )
37
+
38
+ KIN_MEAN = torch.tensor([0.43216, 0.394666, 0.37645]).view(3, 1, 1)
39
+ KIN_STD = torch.tensor([0.22803, 0.22145, 0.216989]).view(3, 1, 1)
40
+
41
+
42
+ def _normalize_state_dict(loaded_obj: object) -> Dict[str, torch.Tensor]:
43
+ if isinstance(loaded_obj, dict):
44
+ for key in ("state_dict", "model_state_dict", "model"):
45
+ nested = loaded_obj.get(key)
46
+ if isinstance(nested, dict):
47
+ loaded_obj = nested
48
+ break
49
+ if not isinstance(loaded_obj, dict):
50
+ raise ValueError("Checkpoint does not contain a valid state dict.")
51
+
52
+ normalized: Dict[str, torch.Tensor] = {}
53
+ for key, value in loaded_obj.items():
54
+ clean_key = key[7:] if key.startswith("module.") else key
55
+ normalized[clean_key] = value
56
+ return normalized
57
+
58
+
59
+ def _load_model() -> torch.nn.Module:
60
+ if not os.path.exists(CKPT_PATH):
61
+ raise FileNotFoundError(
62
+ f"Missing checkpoint: {CKPT_PATH}. Add it to repo root with git-lfs."
63
+ )
64
+
65
+ model = r2plus1d_18(weights=R2Plus1D_18_Weights.DEFAULT)
66
+ model.fc = nn.Linear(model.fc.in_features, NUM_CLASSES)
67
+
68
+ loaded = torch.load(CKPT_PATH, map_location=DEVICE)
69
+ state_dict = _normalize_state_dict(loaded)
70
+ model.load_state_dict(state_dict, strict=True)
71
+ model.to(DEVICE)
72
+ model.eval()
73
+ return model
74
+
75
+
76
+ MODEL = _load_model()
77
+
78
+
79
+ def sample_indices(num_frames: int, clip_len: int = CLIP_LEN) -> np.ndarray:
80
+ if num_frames <= clip_len:
81
+ return np.linspace(0, max(num_frames - 1, 0), clip_len).astype(int)
82
+ start = np.random.randint(0, num_frames - clip_len + 1)
83
+ return np.arange(start, start + clip_len)
84
+
85
+
86
+ def get_clip_tensor(video_reader: VideoReader, inds: np.ndarray) -> torch.Tensor:
87
+ frames = video_reader.get_batch(inds).asnumpy() # (T, H, W, C)
88
+ transformed: List[torch.Tensor] = []
89
+ for i in range(frames.shape[0]):
90
+ frame = EVAL_FRAME_TRANSFORM(frames[i])
91
+ frame = (frame - KIN_MEAN) / KIN_STD
92
+ transformed.append(frame)
93
+
94
+ # (C, T, H, W)
95
+ return torch.stack(transformed, dim=1)
96
+
97
+
98
+ @torch.no_grad()
99
+ def predict_path(video_path: str, n_clips: int = 10) -> Dict[str, object]:
100
+ vr = VideoReader(video_path, ctx=cpu(0))
101
+ total_frames = len(vr)
102
+ if total_frames == 0:
103
+ raise ValueError("Video has no readable frames.")
104
+
105
+ logits_sum = torch.zeros((1, NUM_CLASSES), device=DEVICE)
106
+ for _ in range(n_clips):
107
+ inds = sample_indices(total_frames, CLIP_LEN)
108
+ clip = get_clip_tensor(vr, inds).unsqueeze(0).to(DEVICE)
109
+ logits_sum += MODEL(clip)
110
+
111
+ logits = logits_sum / n_clips
112
+ probs = torch.softmax(logits, dim=1)[0].detach().cpu().numpy().tolist()
113
+ pred = int(np.argmax(probs))
114
+
115
+ return {
116
+ "prediction": "Stress (1)" if pred == 1 else "Normal (0)",
117
+ "probs": {"normal": probs[0], "stress": probs[1]},
118
+ "n_clips": n_clips,
119
+ }
120
+
121
+
122
+ @app.get("/")
123
+ def health() -> Dict[str, str]:
124
+ return {"status": "ok"}
125
+
126
+
127
+ @app.post("/predict")
128
+ async def predict_api(file: UploadFile = File(...)) -> Dict[str, object]:
129
+ suffix = os.path.splitext(file.filename or "")[1] or ".mp4"
130
+
131
+ with NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
132
+ tmp.write(await file.read())
133
+ tmp_path = tmp.name
134
+
135
+ try:
136
+ return predict_path(tmp_path)
137
+ except Exception as exc:
138
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
139
+ finally:
140
+ try:
141
+ os.remove(tmp_path)
142
+ except OSError:
143
+ pass
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ python-multipart
4
+ torch
5
+ torchvision
6
+ numpy
7
+ pillow
8
+ decord