Skip to content

Commit cd7e968

Browse files
rohan-pandeyyrahulharpal1603
authored andcommitted
app size optimization enhancements
- Add an explicit artifact check that fails if any .onnx file is still bundled. - Use from __future__ import annotations for Python <3.9 compatibility. - DELETE /{model_key} delete guard against active sessions. - De-duplication for concurrent /setup or /download/{model_key} for the same model. - SSE heartbeat interval. - Nested HTTP streams on 416 raise_for_status() ordering. - Fix divide-by-zero edge case.
1 parent 03cf632 commit cd7e968

20 files changed

Lines changed: 379 additions & 166 deletions

File tree

.github/workflows/build-and-release.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ jobs:
3434
cd backend
3535
pyinstaller PictoPy.spec
3636
37+
- name: Verify no .onnx artifacts
38+
shell: pwsh
39+
run: |
40+
$onnxFiles = Get-ChildItem -Path "backend/dist/PictoPy_Server" -Recurse -Filter "*.onnx" -File
41+
if ($onnxFiles) {
42+
Write-Error "Found bundled .onnx artifacts in backend/dist/PictoPy_Server:"
43+
$onnxFiles | ForEach-Object { Write-Error $_.FullName }
44+
exit 1
45+
}
46+
3747
- name: Create ZIP package
3848
run: |
3949
cd backend/dist/PictoPy_Server
@@ -69,6 +79,16 @@ jobs:
6979
cd backend
7080
pyinstaller PictoPy.spec
7181
82+
- name: Verify no .onnx artifacts
83+
shell: bash
84+
run: |
85+
onnx_files=$(find backend/dist/PictoPy_Server -type f -name "*.onnx")
86+
if [ -n "$onnx_files" ]; then
87+
echo "Found bundled .onnx artifacts in backend/dist/PictoPy_Server:"
88+
echo "$onnx_files"
89+
exit 1
90+
fi
91+
7292
- name: Create ZIP package
7393
run: |
7494
cd backend/dist/PictoPy_Server
@@ -102,6 +122,16 @@ jobs:
102122
cd backend
103123
pyinstaller PictoPy.spec
104124
125+
- name: Verify no .onnx artifacts
126+
shell: bash
127+
run: |
128+
onnx_files=$(find backend/dist/PictoPy_Server -type f -name "*.onnx")
129+
if [ -n "$onnx_files" ]; then
130+
echo "Found bundled .onnx artifacts in backend/dist/PictoPy_Server:"
131+
echo "$onnx_files"
132+
exit 1
133+
fi
134+
105135
- name: Create ZIP package
106136
run: |
107137
cd backend/dist/PictoPy_Server

backend/PictoPy.spec

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ a = Analysis(
1616
noarchive=False,
1717
)
1818

19-
# Filter out models from the build in case any linger in the development directory
20-
# We ensure the distribution bundle remains lightweight.
2119
def exclude_models(datas):
2220
filtered_datas = []
2321
for data in datas:

backend/app/models/FaceNet.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
from __future__ import annotations
2+
13
import os
24
import threading
35
import onnxruntime
4-
from app.models.model_registry import MODEL_REGISTRY
6+
from app.models.model_registry import MODEL_REGISTRY, get_model_key_from_path
7+
from app.models.session_registry import (
8+
mark_model_session_active,
9+
mark_model_session_inactive,
10+
)
511
from app.utils.FaceNet import FaceNet_util_normalize_embedding
612
from app.utils.ONNX import ONNX_util_get_execution_providers
713
from app.logging.setup_logging import get_logger
@@ -12,14 +18,14 @@
1218
class FaceNet:
1319
def __init__(self, model_path):
1420
self.model_path = model_path
21+
self._model_key = get_model_key_from_path(model_path)
22+
self._session_registered = False
1523
self._session: onnxruntime.InferenceSession | None = None
1624
self.input_tensor_name: str | None = None
1725
self.output_tensor_name: str | None = None
1826
self._lock = threading.Lock()
1927

2028
def get_session(self) -> onnxruntime.InferenceSession:
21-
# Fast path: capture reference before returning so close() on another
22-
# thread between the check and the return cannot cause a None return.
2329
session = self._session
2430
if session is not None:
2531
return session
@@ -43,11 +49,12 @@ def get_session(self) -> onnxruntime.InferenceSession:
4349
self._session = onnxruntime.InferenceSession(
4450
self.model_path, providers=ONNX_util_get_execution_providers()
4551
)
52+
if self._model_key is not None and not self._session_registered:
53+
mark_model_session_active(self._model_key)
54+
self._session_registered = True
4655
self.input_tensor_name = self._session.get_inputs()[0].name
4756
self.output_tensor_name = self._session.get_outputs()[0].name
4857

49-
# Capture inside the lock before releasing — prevents close() on
50-
# another thread from nulling _session between lock release and return.
5158
session = self._session
5259

5360
return session
@@ -66,4 +73,13 @@ def close(self):
6673
self._session = None
6774
self.input_tensor_name = None
6875
self.output_tensor_name = None
76+
if self._model_key is not None and self._session_registered:
77+
mark_model_session_inactive(self._model_key)
78+
self._session_registered = False
6979
logger.info("FaceNet model session closed.")
80+
81+
def __del__(self):
82+
try:
83+
self.close()
84+
except Exception:
85+
pass

backend/app/models/ObjectClassifier.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import cv2
24
from app.models.YOLO import YOLO
35
from app.utils.YOLO import YOLO_util_get_model_path

backend/app/models/YOLO.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
from __future__ import annotations
2+
13
import onnxruntime
24
import time
35
import cv2
46
import numpy as np
7+
from app.models.model_registry import MODEL_REGISTRY, get_model_key_from_path
8+
from app.models.session_registry import (
9+
mark_model_session_active,
10+
mark_model_session_inactive,
11+
)
512
from app.utils.YOLO import (
613
YOLO_util_xywh2xyxy,
714
YOLO_util_draw_detections,
@@ -17,6 +24,8 @@
1724
class YOLO:
1825
def __init__(self, path, conf_threshold=0.7, iou_threshold=0.5):
1926
self.model_path = path
27+
self._model_key = get_model_key_from_path(path)
28+
self._session_registered = False
2029
self.conf_threshold = conf_threshold
2130
self.iou_threshold = iou_threshold
2231
self._session = None
@@ -31,7 +40,6 @@ def get_session(self):
3140
with self._lock:
3241
if self._session is None:
3342
import os
34-
from app.models.model_registry import MODEL_REGISTRY
3543

3644
if not os.path.exists(self.model_path):
3745
model_key = None
@@ -48,6 +56,9 @@ def get_session(self):
4856
self._session = onnxruntime.InferenceSession(
4957
self.model_path, providers=ONNX_util_get_execution_providers()
5058
)
59+
if self._model_key is not None and not self._session_registered:
60+
mark_model_session_active(self._model_key)
61+
self._session_registered = True
5162
# Initialize model info once session is created
5263
self.get_input_details()
5364
self.get_output_details()
@@ -61,8 +72,17 @@ def close(self):
6172
with self._lock:
6273
if self._session is not None:
6374
self._session = None
75+
if self._model_key is not None and self._session_registered:
76+
mark_model_session_inactive(self._model_key)
77+
self._session_registered = False
6478
logger.info("YOLO model session closed.")
6579

80+
def __del__(self):
81+
try:
82+
self.close()
83+
except Exception:
84+
pass
85+
6686
@log_memory_usage
6787
def detect_objects(self, image):
6888
session = self.get_session()

backend/app/models/model_registry.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
from typing import TypedDict, Literal
24
import json
35
import os
@@ -106,3 +108,11 @@ def get_model_path(key: str) -> str:
106108
# In development, strictly use the local repo folder
107109
return os.path.normpath(os.path.join(LOCAL_ONNX_EXPORTS, filename))
108110

111+
112+
def get_model_key_from_path(model_path: str) -> str | None:
113+
target_filename = os.path.basename(os.path.normpath(model_path)).lower()
114+
for key, spec in MODEL_REGISTRY.items():
115+
if spec["filename"].lower() == target_filename:
116+
return key
117+
return None
118+
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from __future__ import annotations
2+
3+
import threading
4+
5+
_active_sessions: dict[str, int] = {}
6+
_registry_lock = threading.Lock()
7+
8+
9+
def mark_model_session_active(model_key: str) -> None:
10+
with _registry_lock:
11+
_active_sessions[model_key] = _active_sessions.get(model_key, 0) + 1
12+
13+
14+
def mark_model_session_inactive(model_key: str) -> None:
15+
with _registry_lock:
16+
count = _active_sessions.get(model_key, 0)
17+
if count <= 1:
18+
_active_sessions.pop(model_key, None)
19+
return
20+
_active_sessions[model_key] = count - 1
21+
22+
23+
def get_active_session_count(model_key: str) -> int:
24+
with _registry_lock:
25+
return _active_sessions.get(model_key, 0)

0 commit comments

Comments
 (0)