nedo-vision-worker-core 0.4.1__py3-none-any.whl → 0.4.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nedo-vision-worker-core might be problematic. Click here for more details.

@@ -7,7 +7,7 @@ A library for running AI vision processing and detection in the Nedo Vision plat
7
7
  from .core_service import CoreService
8
8
  from .callbacks import DetectionType, CallbackTrigger, DetectionData, IntervalMetadata
9
9
 
10
- __version__ = "0.4.1"
10
+ __version__ = "0.4.4"
11
11
  __all__ = [
12
12
  "CoreService",
13
13
  "DetectionType",
@@ -31,6 +31,11 @@ class PipelineManager:
31
31
  self._stop_lock = threading.Lock()
32
32
  self.on_pipeline_stopped = on_pipeline_stopped
33
33
 
34
+ # Stagger pipeline startup to reduce CPU spikes
35
+ self._last_pipeline_start = 0
36
+ self._pipeline_start_delay = 1.0 # 1 second between pipeline starts
37
+ self._start_lock = threading.Lock()
38
+
34
39
  logging.info(f"🚀 PipelineManager initialized with {max_workers} worker threads")
35
40
 
36
41
  def start_pipeline(self, pipeline, detector):
@@ -46,6 +51,15 @@ class PipelineManager:
46
51
  logging.warning(f"⚠️ Pipeline {pipeline_id} is already running.")
47
52
  return
48
53
 
54
+ # Stagger pipeline starts to reduce CPU spikes
55
+ with self._start_lock:
56
+ time_since_last_start = time.time() - self._last_pipeline_start
57
+ if time_since_last_start < self._pipeline_start_delay:
58
+ delay = self._pipeline_start_delay - time_since_last_start
59
+ logging.info(f"⏳ Staggering pipeline {pipeline_id} start by {delay:.2f}s to reduce CPU spike")
60
+ time.sleep(delay)
61
+ self._last_pipeline_start = time.time()
62
+
49
63
  logging.info(f"🚀 Starting Pipeline processing for pipeline: {pipeline_id} | Source: {worker_source_id} ({pipeline.name})")
50
64
 
51
65
  # Acquire video stream
@@ -47,6 +47,10 @@ class PipelineProcessor:
47
47
  self.last_preview_check_time = 0
48
48
  self.preview_check_interval = 10.0 # Check every 10 seconds (reduced from 5s to save CPU)
49
49
  self.pipeline_repo = WorkerSourcePipelineRepository()
50
+
51
+ # RTMP frame rate limiting to reduce CPU
52
+ self.last_rtmp_frame_time = 0
53
+ self.rtmp_frame_interval = 1.0 / 25.0 # 25 FPS for RTMP (matching render FPS)
50
54
 
51
55
  self.detection_processor_codes = [
52
56
  PPEDetectionProcessor.code,
@@ -198,10 +202,15 @@ class PipelineProcessor:
198
202
  if loop_start - last_preview_check >= self.preview_check_interval:
199
203
  self._check_and_update_rtmp_streaming()
200
204
  last_preview_check = loop_start
201
-
202
- should_draw = self.rtmp_streaming_active or self.debug_flag
203
205
 
204
- if should_draw:
206
+ # For RTMP, also check if it's time to send a frame (rate limiting)
207
+ should_draw_for_rtmp = (
208
+ self.rtmp_streaming_active and
209
+ self.rtmp_streamer is not None and
210
+ (loop_start - self.last_rtmp_frame_time >= self.rtmp_frame_interval)
211
+ )
212
+
213
+ if should_draw_for_rtmp or self.debug_flag:
205
214
  try:
206
215
  frame_to_draw = frame.copy()
207
216
  self.frame_drawer.draw_polygons(frame_to_draw)
@@ -215,7 +224,7 @@ class PipelineProcessor:
215
224
  logging.error(f"❌ Draw failed, using raw frame: {e}")
216
225
  drawn_frame = frame
217
226
  else:
218
- drawn_frame = frame
227
+ drawn_frame = None # Don't waste CPU drawing if not needed
219
228
 
220
229
  if self.debug_flag:
221
230
  tracked_objects_render = self._process_frame(frame)
@@ -235,13 +244,16 @@ class PipelineProcessor:
235
244
  try:
236
245
  self.rtmp_streamer = RTMPStreamer(self.pipeline_id)
237
246
  logging.info(f"🎬 RTMP streamer initialized for pipeline {pipeline_id} (preview requested)")
247
+ self.last_rtmp_frame_time = 0 # Reset frame time on new stream
238
248
  except Exception as e:
239
249
  logging.error(f"❌ Failed to initialize RTMP streamer for pipeline {pipeline_id}: {e}")
240
250
  self.rtmp_streamer = None
241
251
 
242
- if self.rtmp_streamer:
252
+ if self.rtmp_streamer and drawn_frame is not None:
253
+ # Frame already rate-limited by drawing logic above
243
254
  try:
244
255
  self.rtmp_streamer.push_frame(drawn_frame)
256
+ self.last_rtmp_frame_time = loop_start
245
257
  except Exception as e:
246
258
  logging.error(f"❌ RTMP push error for pipeline {pipeline_id}: {e}")
247
259
  if "initialization_failed" in str(e).lower():
@@ -5,6 +5,7 @@ import threading
5
5
  from typing import Dict, Set, Optional
6
6
  from ..repositories.WorkerSourcePipelineDebugRepository import WorkerSourcePipelineDebugRepository
7
7
  from ..repositories.WorkerSourcePipelineRepository import WorkerSourcePipelineRepository
8
+ from ..repositories.WorkerSourceRepository import WorkerSourceRepository
8
9
  from .PipelineManager import PipelineManager
9
10
  from .ModelManager import ModelManager
10
11
  from ..streams.VideoStreamManager import VideoStreamManager
@@ -19,6 +20,7 @@ class PipelineSyncThread(threading.Thread):
19
20
  self.polling_interval = polling_interval
20
21
  self.pipeline_repo = WorkerSourcePipelineRepository()
21
22
  self.debug_repo = WorkerSourcePipelineDebugRepository()
23
+ self.source_repo = WorkerSourceRepository()
22
24
  self.model_manager = ModelManager()
23
25
  self.running = True
24
26
  self.pipeline_manager = PipelineManager(video_manager, self.on_pipeline_stopped, max_workers)
@@ -82,6 +84,11 @@ class PipelineSyncThread(threading.Thread):
82
84
  pipeline.pipeline_status_code = 'run'
83
85
 
84
86
  if pipeline.pipeline_status_code == 'run':
87
+ # Check if source is connected before starting pipeline
88
+ if not self.source_repo.is_source_connected(pipeline.worker_source_id):
89
+ logging.warning(f"⚠️ Skipping pipeline {pid} ({pipeline.name}): Source {pipeline.worker_source_id} is disconnected")
90
+ continue
91
+
85
92
  detector = self.model_manager.get_detector(pipeline.ai_model_id)
86
93
 
87
94
  if not detector and pipeline.ai_model_id:
@@ -144,8 +151,15 @@ class PipelineSyncThread(threading.Thread):
144
151
  ])
145
152
 
146
153
  if requires_restart:
154
+ # Check if source is connected before restarting
155
+ if not self.source_repo.is_source_connected(db_pipeline.worker_source_id):
156
+ logging.warning(f"⚠️ Cannot restart pipeline {pid}: Source {db_pipeline.worker_source_id} is disconnected")
157
+ return
158
+
147
159
  logging.info(f"🔄 Restarting pipeline due to significant changes: {pid}")
148
160
  self.pipeline_manager.stop_pipeline(pid)
161
+
162
+
149
163
  self.pipeline_manager.start_pipeline(db_pipeline, db_detector)
150
164
  else:
151
165
  # Update config for minor changes that don't require restart
@@ -18,12 +18,14 @@ class WorkerSourcePipelineDetectionRepository(BaseRepository):
18
18
  def save_detection(self, pipeline_id: int, frame, tracked_objects, frame_drawer: FrameDrawer):
19
19
  """
20
20
  Save detection data that need to be sent to database.
21
+ Only saves if there are violations detected.
21
22
  """
22
23
  now = datetime.now(timezone.utc)
23
24
  current_datetime = now.strftime("%Y%m%d_%H%M%S")
24
25
 
25
26
  frame_drawer.draw_polygons(frame)
26
27
  filtered_objects = []
28
+ has_violations = False
27
29
 
28
30
  for tracked_obj in tracked_objects:
29
31
  attributes = tracked_obj["attributes"]
@@ -34,9 +36,17 @@ class WorkerSourcePipelineDetectionRepository(BaseRepository):
34
36
  obj = tracked_obj.copy()
35
37
  obj["attributes"] = [attr for attr in attributes if attr.get("count", 0) >= 5]
36
38
 
39
+ # Check if any attribute is a violation
40
+ for attr in obj["attributes"]:
41
+ attr_label = attr.get("label", "")
42
+ if attr_label in frame_drawer.violation_labels:
43
+ has_violations = True
44
+ break
45
+
37
46
  filtered_objects.append(obj)
38
47
 
39
- if not filtered_objects:
48
+ # Only save and trigger webhook/MQTT if there are actual violations
49
+ if not filtered_objects or not has_violations:
40
50
  return
41
51
 
42
52
  drawn_frame = frame_drawer.draw_frame(frame.copy(), filtered_objects)
@@ -64,7 +74,5 @@ class WorkerSourcePipelineDetectionRepository(BaseRepository):
64
74
  )
65
75
  session.add(new_detection)
66
76
  session.flush()
67
- # Commit happens automatically via context manager
68
- print(f"✅ Inserted detection data for pipeline {pipeline_id}")
69
77
  except Exception as e:
70
78
  print(f"❌ Database error while saving detection: {e}")
@@ -19,3 +19,38 @@ class WorkerSourceRepository(BaseRepository):
19
19
  for source in sources:
20
20
  session.expunge(source)
21
21
  return sources
22
+
23
+ def get_worker_source(self, source_id: str):
24
+ """
25
+ Fetch a single worker source by ID.
26
+
27
+ Args:
28
+ source_id (str): The worker source ID
29
+
30
+ Returns:
31
+ WorkerSourceEntity: The worker source entity or None if not found
32
+ """
33
+ with self._get_session() as session:
34
+ session.expire_all()
35
+ source = session.query(WorkerSourceEntity).filter(
36
+ WorkerSourceEntity.id == source_id
37
+ ).first()
38
+ if source:
39
+ session.expunge(source)
40
+ return source
41
+
42
+ def is_source_connected(self, source_id: str) -> bool:
43
+ """
44
+ Check if a worker source is connected.
45
+
46
+ Args:
47
+ source_id (str): The worker source ID
48
+
49
+ Returns:
50
+ bool: True if source is connected, False otherwise
51
+ """
52
+ source = self.get_worker_source(source_id)
53
+ if not source:
54
+ return False
55
+
56
+ return source.status_code == "connected" if source.status_code else False
@@ -25,7 +25,7 @@ class RTMPStreamer:
25
25
  # Class-level lock to stagger stream initialization across all instances
26
26
  _initialization_lock = threading.Lock()
27
27
  _last_initialization_time = 0
28
- _min_initialization_delay = 0.5 # 500ms between stream starts
28
+ _min_initialization_delay = 1.5 # 1.5 seconds between stream starts (increased from 0.5s)
29
29
 
30
30
  def __init__(self, pipeline_id: str, fps: int = 25, bitrate: str = "1500k"):
31
31
  self.pipeline_id = pipeline_id
@@ -237,6 +237,8 @@ class VideoStream(threading.Thread):
237
237
  def run(self) -> None:
238
238
  """Main capture loop."""
239
239
  failures = 0
240
+ last_frame_time = time.perf_counter()
241
+
240
242
  while self.running:
241
243
  try:
242
244
  if not self.capture or not self.capture.isOpened():
@@ -247,6 +249,18 @@ class VideoStream(threading.Thread):
247
249
  failures = 0
248
250
  self._reconnect_attempts = 0
249
251
  self._current_interval = self.reconnect_interval
252
+ last_frame_time = time.perf_counter()
253
+
254
+ # For video files, respect the FPS to avoid playing too fast
255
+ if self.is_file:
256
+ target_fps = self.target_fps if self.target_fps else self.fps
257
+ if target_fps > 0:
258
+ frame_interval = 1.0 / target_fps
259
+ elapsed = time.perf_counter() - last_frame_time
260
+ if elapsed < frame_interval:
261
+ sleep_time = frame_interval - elapsed
262
+ time.sleep(sleep_time)
263
+ last_frame_time = time.perf_counter()
250
264
 
251
265
  ret, frame = self.capture.read()
252
266
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nedo-vision-worker-core
3
- Version: 0.4.1
3
+ Version: 0.4.4
4
4
  Summary: Nedo Vision Worker Core Library for AI Vision Processing
5
5
  Author-email: Willy Achmat Fauzi <willy.achmat@gmail.com>
6
6
  Maintainer-email: Willy Achmat Fauzi <willy.achmat@gmail.com>
@@ -1,4 +1,4 @@
1
- nedo_vision_worker_core/__init__.py,sha256=PMh9hXyCQ2JPaLxq5mFDqRZonzYLfZnwwtGukl0GVBM,1923
1
+ nedo_vision_worker_core/__init__.py,sha256=ZlpHZZ54eVsepVdpBxhuJHhyEpKF8OqZAP9UvfFjMa4,1923
2
2
  nedo_vision_worker_core/cli.py,sha256=8YuKWsIgICUYXE_QtwyU3WzGhVjTWiAo5uzpFOmjNc8,5766
3
3
  nedo_vision_worker_core/core_service.py,sha256=q8-GuGW_l5l6wTWQDqc7BDdhM7zKC-mMLZ5wIHu9xV0,11628
4
4
  nedo_vision_worker_core/doctor.py,sha256=K_-hVV2-mdEefZ4Cfu5hMCiOxBiI1aXY8VtkkpK80Lc,10651
@@ -59,10 +59,10 @@ nedo_vision_worker_core/models/worker_source_pipeline_debug.py,sha256=6S7TkN37Fr
59
59
  nedo_vision_worker_core/models/worker_source_pipeline_detection.py,sha256=p6CJsiVCKprTYrNxJsiTB8njXdHkjZKVEyBceRVE6fY,560
60
60
  nedo_vision_worker_core/pipeline/ModelManager.py,sha256=2DoQiIdF-PAqU7nT_u6bj-DY0aT2FHb8kt24okGGCRc,7449
61
61
  nedo_vision_worker_core/pipeline/PipelineConfigManager.py,sha256=X55i9GyXcW9ylO6cj2UMAZFSxxPViacL4H4DZl60CAY,1157
62
- nedo_vision_worker_core/pipeline/PipelineManager.py,sha256=3I9UBJu_rRfTEctwj8i4hO4MHjpBtYpfh-rIi64qgEw,7638
62
+ nedo_vision_worker_core/pipeline/PipelineManager.py,sha256=AlDwBYYRPeAeh2ilmC8n-A_2gYPqAzeSSVpR1Tc0ipE,8366
63
63
  nedo_vision_worker_core/pipeline/PipelinePrepocessor.py,sha256=cCiVSHHqsKCtKYURdYoEjHJX2GnT6zd8kQ6ZukjQ3V0,1271
64
- nedo_vision_worker_core/pipeline/PipelineProcessor.py,sha256=qjAviYziFX9zJbRDIx7me94ZEccA1r53bunuDySTPhQ,34356
65
- nedo_vision_worker_core/pipeline/PipelineSyncThread.py,sha256=HkW6wj0eDr6M1K3Y25IlB2V6tpIZsKA34AM49AXvcQk,8707
64
+ nedo_vision_worker_core/pipeline/PipelineProcessor.py,sha256=5j0FvGwBw5hgJ-FzI493cLpZomfYiG-bmUZZyklmahI,35149
65
+ nedo_vision_worker_core/pipeline/PipelineSyncThread.py,sha256=8HOVdeWO0dAXY9rMAk4VlX-bIhWrkhSzyJwlJobUnvw,9517
66
66
  nedo_vision_worker_core/pipeline/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
67
67
  nedo_vision_worker_core/preprocessing/ImageResizer.py,sha256=RvOazxe6dJQuiy0ZH4lIGbdFfiu0FLUVCHoMvxkDNT4,1324
68
68
  nedo_vision_worker_core/preprocessing/ImageRoi.py,sha256=iO7oQ-SdUSA_kTIVBuq_mdycXsiJNfiFD3J7-VTxiQ4,2141
@@ -73,17 +73,17 @@ nedo_vision_worker_core/repositories/BaseRepository.py,sha256=FvdcD8I8_4_6TMZa8X
73
73
  nedo_vision_worker_core/repositories/PPEDetectionRepository.py,sha256=dIOPUU7xOCJ8A5v6AnBVKwtKPVlc5M5cUhLCWumTXyo,6882
74
74
  nedo_vision_worker_core/repositories/RestrictedAreaRepository.py,sha256=a5Vc8WLTtAa6Tn-ZSKkBEw4-cM29VW8WUwhbH7YfM9E,4416
75
75
  nedo_vision_worker_core/repositories/WorkerSourcePipelineDebugRepository.py,sha256=lN_yip6woya9YUA5sYKbTyDQz2qSfgqkr3YP2hSd9ws,3211
76
- nedo_vision_worker_core/repositories/WorkerSourcePipelineDetectionRepository.py,sha256=5m4lvmIETJSGDH1T1EHuUDWC-13t5I860UbN_uzEj9A,2641
76
+ nedo_vision_worker_core/repositories/WorkerSourcePipelineDetectionRepository.py,sha256=AVmrpeu6YziX2255_ZhK-5X5snxVMlr8xzb4ymAe5SA,2954
77
77
  nedo_vision_worker_core/repositories/WorkerSourcePipelineRepository.py,sha256=vwwRA1INuK66siOHNZxSBX8CE9uEW8VVcCIA7dmshKo,4714
78
- nedo_vision_worker_core/repositories/WorkerSourceRepository.py,sha256=-a-UlsopPlJWlY36QUodPEjSZVE3BDoLgsVAioiNOo0,663
78
+ nedo_vision_worker_core/repositories/WorkerSourceRepository.py,sha256=YFevfYhAsYd7Eho1iagzjk67tKAQfqmoIExyxvR2Bzc,1760
79
79
  nedo_vision_worker_core/repositories/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
80
80
  nedo_vision_worker_core/services/SharedVideoStreamServer.py,sha256=rhCineMKPG3GQbrMHlSHP4xhXaGZ6Rn1oqIajW5xpaY,9827
81
81
  nedo_vision_worker_core/services/VideoSharingDaemon.py,sha256=iY6afEKTOsphfHvmZTL0grezka2DS9DDq-1EIpVMy0Y,28524
82
82
  nedo_vision_worker_core/services/VideoSharingDaemonManager.py,sha256=sc8VZo5iwoOdR8uTiel5BKz6-eZ7wwLy3IwV_3tsAu0,10340
83
- nedo_vision_worker_core/streams/RTMPStreamer.py,sha256=X6QL84VdjKd995BwpcvD99sCBJGxj4MXWI0q9zo5Izw,18761
83
+ nedo_vision_worker_core/streams/RTMPStreamer.py,sha256=7vg2as_TtmZavLnzhEUOv6HhjoGH5X3JO9HUI74meNs,18789
84
84
  nedo_vision_worker_core/streams/SharedVideoDeviceManager.py,sha256=vSslwxbhKH6FPndR1HcSFIVWtF-iiOQMlSa4VvFa6M4,16265
85
85
  nedo_vision_worker_core/streams/StreamSyncThread.py,sha256=ETT0N_P90ksn6Q5pb7NvMadqCuoicz_g52lcDkHIp88,5382
86
- nedo_vision_worker_core/streams/VideoStream.py,sha256=nGtJ4FAZ1Ek-8hVRopEt0bLWLpa10OtyUwdDEuXLObQ,13343
86
+ nedo_vision_worker_core/streams/VideoStream.py,sha256=Xg3gxa9JZfCkwqD5OnvNmvf_iKmlEpjTlk4_YS2f2xU,14046
87
87
  nedo_vision_worker_core/streams/VideoStreamManager.py,sha256=g5cz-YXPewSubBXxCg4mfzsuGKoOHXu-SrMxaGjYPHw,14956
88
88
  nedo_vision_worker_core/streams/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
89
89
  nedo_vision_worker_core/tracker/SFSORT.py,sha256=0kggw0l4yPZ55AKHdqVX6mu9ehHmJed7jcJ3JQoC4sk,14061
@@ -98,8 +98,8 @@ nedo_vision_worker_core/util/PipelinePreviewChecker.py,sha256=XxlSMlrDlRrzfV8_Y-
98
98
  nedo_vision_worker_core/util/PlatformDetector.py,sha256=GGL8UfeMQITR22EMYIRWnuOEnSqo7Dr5mb0PaFrl8AM,3006
99
99
  nedo_vision_worker_core/util/TablePrinter.py,sha256=wzLGgb1GFMeIbAP6HmKcZD33j4D-IlyqlyeR7C5yD7w,1137
100
100
  nedo_vision_worker_core/util/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
101
- nedo_vision_worker_core-0.4.1.dist-info/METADATA,sha256=eVzhZ0Gwb-Rd1lIasDKRUrGAfrDzqPgiTfR54rg6Vl8,14426
102
- nedo_vision_worker_core-0.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
103
- nedo_vision_worker_core-0.4.1.dist-info/entry_points.txt,sha256=pIPafsvPnBw-fpBKBmc1NQCQ6PQY3ad8mZ6mn8_p5FI,70
104
- nedo_vision_worker_core-0.4.1.dist-info/top_level.txt,sha256=y8kusXjVYqtG8MSHYWTrk8bRrvjOrphKXYyzu943TTQ,24
105
- nedo_vision_worker_core-0.4.1.dist-info/RECORD,,
101
+ nedo_vision_worker_core-0.4.4.dist-info/METADATA,sha256=NeX_eRQ8lbk8z9HYN0ERFouzwjnbdc_xLXytxzvrQtw,14426
102
+ nedo_vision_worker_core-0.4.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
103
+ nedo_vision_worker_core-0.4.4.dist-info/entry_points.txt,sha256=pIPafsvPnBw-fpBKBmc1NQCQ6PQY3ad8mZ6mn8_p5FI,70
104
+ nedo_vision_worker_core-0.4.4.dist-info/top_level.txt,sha256=y8kusXjVYqtG8MSHYWTrk8bRrvjOrphKXYyzu943TTQ,24
105
+ nedo_vision_worker_core-0.4.4.dist-info/RECORD,,