nedo-vision-worker 1.0.0__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.
- nedo_vision_worker/__init__.py +10 -0
- nedo_vision_worker/cli.py +195 -0
- nedo_vision_worker/config/ConfigurationManager.py +196 -0
- nedo_vision_worker/config/__init__.py +1 -0
- nedo_vision_worker/database/DatabaseManager.py +219 -0
- nedo_vision_worker/database/__init__.py +1 -0
- nedo_vision_worker/doctor.py +453 -0
- nedo_vision_worker/initializer/AppInitializer.py +78 -0
- nedo_vision_worker/initializer/__init__.py +1 -0
- nedo_vision_worker/models/__init__.py +15 -0
- nedo_vision_worker/models/ai_model.py +29 -0
- nedo_vision_worker/models/auth.py +14 -0
- nedo_vision_worker/models/config.py +9 -0
- nedo_vision_worker/models/dataset_source.py +30 -0
- nedo_vision_worker/models/logs.py +9 -0
- nedo_vision_worker/models/ppe_detection.py +39 -0
- nedo_vision_worker/models/ppe_detection_label.py +20 -0
- nedo_vision_worker/models/restricted_area_violation.py +20 -0
- nedo_vision_worker/models/user.py +10 -0
- nedo_vision_worker/models/worker_source.py +19 -0
- nedo_vision_worker/models/worker_source_pipeline.py +21 -0
- nedo_vision_worker/models/worker_source_pipeline_config.py +24 -0
- nedo_vision_worker/models/worker_source_pipeline_debug.py +15 -0
- nedo_vision_worker/models/worker_source_pipeline_detection.py +14 -0
- nedo_vision_worker/protos/AIModelService_pb2.py +46 -0
- nedo_vision_worker/protos/AIModelService_pb2_grpc.py +140 -0
- nedo_vision_worker/protos/DatasetSourceService_pb2.py +46 -0
- nedo_vision_worker/protos/DatasetSourceService_pb2_grpc.py +140 -0
- nedo_vision_worker/protos/HumanDetectionService_pb2.py +44 -0
- nedo_vision_worker/protos/HumanDetectionService_pb2_grpc.py +140 -0
- nedo_vision_worker/protos/PPEDetectionService_pb2.py +46 -0
- nedo_vision_worker/protos/PPEDetectionService_pb2_grpc.py +140 -0
- nedo_vision_worker/protos/VisionWorkerService_pb2.py +72 -0
- nedo_vision_worker/protos/VisionWorkerService_pb2_grpc.py +471 -0
- nedo_vision_worker/protos/WorkerSourcePipelineService_pb2.py +64 -0
- nedo_vision_worker/protos/WorkerSourcePipelineService_pb2_grpc.py +312 -0
- nedo_vision_worker/protos/WorkerSourceService_pb2.py +50 -0
- nedo_vision_worker/protos/WorkerSourceService_pb2_grpc.py +183 -0
- nedo_vision_worker/protos/__init__.py +1 -0
- nedo_vision_worker/repositories/AIModelRepository.py +44 -0
- nedo_vision_worker/repositories/DatasetSourceRepository.py +150 -0
- nedo_vision_worker/repositories/PPEDetectionRepository.py +112 -0
- nedo_vision_worker/repositories/RestrictedAreaRepository.py +88 -0
- nedo_vision_worker/repositories/WorkerSourcePipelineDebugRepository.py +90 -0
- nedo_vision_worker/repositories/WorkerSourcePipelineDetectionRepository.py +48 -0
- nedo_vision_worker/repositories/WorkerSourcePipelineRepository.py +174 -0
- nedo_vision_worker/repositories/WorkerSourceRepository.py +46 -0
- nedo_vision_worker/repositories/__init__.py +1 -0
- nedo_vision_worker/services/AIModelClient.py +362 -0
- nedo_vision_worker/services/ConnectionInfoClient.py +57 -0
- nedo_vision_worker/services/DatasetSourceClient.py +88 -0
- nedo_vision_worker/services/FileToRTMPServer.py +78 -0
- nedo_vision_worker/services/GrpcClientBase.py +155 -0
- nedo_vision_worker/services/GrpcClientManager.py +141 -0
- nedo_vision_worker/services/ImageUploadClient.py +82 -0
- nedo_vision_worker/services/PPEDetectionClient.py +108 -0
- nedo_vision_worker/services/RTSPtoRTMPStreamer.py +98 -0
- nedo_vision_worker/services/RestrictedAreaClient.py +100 -0
- nedo_vision_worker/services/SystemUsageClient.py +77 -0
- nedo_vision_worker/services/VideoStreamClient.py +161 -0
- nedo_vision_worker/services/WorkerSourceClient.py +215 -0
- nedo_vision_worker/services/WorkerSourcePipelineClient.py +393 -0
- nedo_vision_worker/services/WorkerSourceUpdater.py +134 -0
- nedo_vision_worker/services/WorkerStatusClient.py +65 -0
- nedo_vision_worker/services/__init__.py +1 -0
- nedo_vision_worker/util/HardwareID.py +104 -0
- nedo_vision_worker/util/ImageUploader.py +92 -0
- nedo_vision_worker/util/Networking.py +94 -0
- nedo_vision_worker/util/PlatformDetector.py +50 -0
- nedo_vision_worker/util/SystemMonitor.py +299 -0
- nedo_vision_worker/util/VideoProbeUtil.py +120 -0
- nedo_vision_worker/util/__init__.py +1 -0
- nedo_vision_worker/worker/CoreActionWorker.py +125 -0
- nedo_vision_worker/worker/DataSenderWorker.py +168 -0
- nedo_vision_worker/worker/DataSyncWorker.py +143 -0
- nedo_vision_worker/worker/DatasetFrameSender.py +208 -0
- nedo_vision_worker/worker/DatasetFrameWorker.py +412 -0
- nedo_vision_worker/worker/PPEDetectionManager.py +86 -0
- nedo_vision_worker/worker/PipelineActionWorker.py +129 -0
- nedo_vision_worker/worker/PipelineImageWorker.py +116 -0
- nedo_vision_worker/worker/RabbitMQListener.py +170 -0
- nedo_vision_worker/worker/RestrictedAreaManager.py +85 -0
- nedo_vision_worker/worker/SystemUsageManager.py +111 -0
- nedo_vision_worker/worker/VideoStreamWorker.py +139 -0
- nedo_vision_worker/worker/WorkerManager.py +155 -0
- nedo_vision_worker/worker/__init__.py +1 -0
- nedo_vision_worker/worker_service.py +264 -0
- nedo_vision_worker-1.0.0.dist-info/METADATA +563 -0
- nedo_vision_worker-1.0.0.dist-info/RECORD +92 -0
- nedo_vision_worker-1.0.0.dist-info/WHEEL +5 -0
- nedo_vision_worker-1.0.0.dist-info/entry_points.txt +2 -0
- nedo_vision_worker-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Dict, Optional
|
|
7
|
+
from ..services.DatasetSourceClient import DatasetSourceClient
|
|
8
|
+
from ..database.DatabaseManager import _get_storage_paths
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
class DatasetFrameSender:
|
|
13
|
+
"""Handles batched sending of saved dataset frames to the backend."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, server_host: str, token: str):
|
|
16
|
+
"""
|
|
17
|
+
Initialize the Dataset Frame Sender.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
server_host (str): Server host for sending frames
|
|
21
|
+
token (str): Authentication token
|
|
22
|
+
"""
|
|
23
|
+
self.server_host = server_host
|
|
24
|
+
self.token = token
|
|
25
|
+
self.client = DatasetSourceClient(server_host)
|
|
26
|
+
|
|
27
|
+
# Get storage paths
|
|
28
|
+
storage_paths = _get_storage_paths()
|
|
29
|
+
self.dataset_frames_path = storage_paths["files"] / "dataset_frames"
|
|
30
|
+
|
|
31
|
+
# Track sent frames to avoid duplicates
|
|
32
|
+
self.sent_frames = set()
|
|
33
|
+
|
|
34
|
+
def send_pending_frames(self, max_batch_size: int = 10) -> Dict[str, int]:
|
|
35
|
+
"""
|
|
36
|
+
Send pending dataset frames in batches.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
max_batch_size (int): Maximum number of frames to send in one batch
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dict[str, int]: Statistics of sent frames per dataset source
|
|
43
|
+
"""
|
|
44
|
+
stats = {}
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
if not self.dataset_frames_path.exists():
|
|
48
|
+
return stats
|
|
49
|
+
|
|
50
|
+
# Find all dataset source directories
|
|
51
|
+
for dataset_source_dir in self.dataset_frames_path.iterdir():
|
|
52
|
+
if not dataset_source_dir.is_dir():
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
dataset_source_id = dataset_source_dir.name
|
|
56
|
+
sent_count = self._send_frames_for_dataset_source(
|
|
57
|
+
dataset_source_dir,
|
|
58
|
+
dataset_source_id,
|
|
59
|
+
max_batch_size
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if sent_count > 0:
|
|
63
|
+
stats[dataset_source_id] = sent_count
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.error(f"🚨 [APP] Error sending pending frames: {e}")
|
|
67
|
+
|
|
68
|
+
return stats
|
|
69
|
+
|
|
70
|
+
def _send_frames_for_dataset_source(self, dataset_source_dir: Path, dataset_source_id: str, max_batch_size: int) -> int:
|
|
71
|
+
"""
|
|
72
|
+
Send frames for a specific dataset source.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
dataset_source_dir (Path): Directory containing frames for this dataset source
|
|
76
|
+
dataset_source_id (str): ID of the dataset source
|
|
77
|
+
max_batch_size (int): Maximum frames to send in one batch
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
int: Number of frames sent
|
|
81
|
+
"""
|
|
82
|
+
sent_count = 0
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# Find all frame files (jpg) that haven't been sent yet
|
|
86
|
+
frame_files = []
|
|
87
|
+
for file_path in dataset_source_dir.glob("*.jpg"):
|
|
88
|
+
frame_uuid = file_path.stem.split('_')[0] # Extract UUID from filename
|
|
89
|
+
|
|
90
|
+
if frame_uuid not in self.sent_frames:
|
|
91
|
+
metadata_path = file_path.with_suffix('.json')
|
|
92
|
+
if metadata_path.exists():
|
|
93
|
+
frame_files.append((file_path, metadata_path, frame_uuid))
|
|
94
|
+
|
|
95
|
+
# Sort by timestamp (filename contains timestamp)
|
|
96
|
+
frame_files.sort(key=lambda x: x[0].name)
|
|
97
|
+
|
|
98
|
+
# Send frames in batches
|
|
99
|
+
for i in range(0, len(frame_files), max_batch_size):
|
|
100
|
+
batch = frame_files[i:i + max_batch_size]
|
|
101
|
+
if self._send_frame_batch(batch, dataset_source_id):
|
|
102
|
+
sent_count += len(batch)
|
|
103
|
+
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.error(f"🚨 [APP] Error sending frames for dataset source {dataset_source_id}: {e}")
|
|
106
|
+
|
|
107
|
+
return sent_count
|
|
108
|
+
|
|
109
|
+
def _send_frame_batch(self, frame_batch: List[tuple], dataset_source_id: str) -> bool:
|
|
110
|
+
"""
|
|
111
|
+
Send a batch of frames to the backend.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
frame_batch (List[tuple]): List of (file_path, metadata_path, frame_uuid) tuples
|
|
115
|
+
dataset_source_id (str): ID of the dataset source
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
bool: True if batch was sent successfully
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
for file_path, metadata_path, frame_uuid in frame_batch:
|
|
122
|
+
# Read frame data
|
|
123
|
+
with open(file_path, 'rb') as f:
|
|
124
|
+
frame_bytes = f.read()
|
|
125
|
+
|
|
126
|
+
# Read metadata
|
|
127
|
+
with open(metadata_path, 'r') as f:
|
|
128
|
+
metadata = json.load(f)
|
|
129
|
+
|
|
130
|
+
# Send frame to backend
|
|
131
|
+
response = self.client.send_dataset_frame(
|
|
132
|
+
dataset_source_id=metadata["dataset_source_id"],
|
|
133
|
+
uuid=metadata["frame_uuid"],
|
|
134
|
+
image=frame_bytes,
|
|
135
|
+
timestamp=metadata["timestamp"],
|
|
136
|
+
token=self.token
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if response and response.get("success"):
|
|
140
|
+
# Mark as sent and clean up local files
|
|
141
|
+
self.sent_frames.add(frame_uuid)
|
|
142
|
+
self._cleanup_sent_frame(file_path, metadata_path)
|
|
143
|
+
logger.debug(f"✅ [APP] Sent frame {frame_uuid} for dataset source {dataset_source_id}")
|
|
144
|
+
else:
|
|
145
|
+
error_message = response.get("message", "Unknown error") if response else "Unknown error"
|
|
146
|
+
|
|
147
|
+
# Handle specific error cases
|
|
148
|
+
if "DatasetSource not found" in error_message:
|
|
149
|
+
logger.warning(f"🗑️ [APP] Dataset source {dataset_source_id} not found, cleaning up orphaned frame {frame_uuid}")
|
|
150
|
+
# Mark as sent to avoid retry loops and clean up
|
|
151
|
+
self.sent_frames.add(frame_uuid)
|
|
152
|
+
self._cleanup_sent_frame(file_path, metadata_path)
|
|
153
|
+
else:
|
|
154
|
+
logger.error(f"❌ [APP] Failed to send frame {frame_uuid}: {error_message}")
|
|
155
|
+
return False
|
|
156
|
+
|
|
157
|
+
return True
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"🚨 [APP] Error sending frame batch: {e}")
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
def _cleanup_sent_frame(self, file_path: Path, metadata_path: Path):
|
|
164
|
+
"""
|
|
165
|
+
Clean up local files after successful send.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
file_path (Path): Path to the frame file
|
|
169
|
+
metadata_path (Path): Path to the metadata file
|
|
170
|
+
"""
|
|
171
|
+
try:
|
|
172
|
+
# Remove frame file
|
|
173
|
+
if file_path.exists():
|
|
174
|
+
file_path.unlink()
|
|
175
|
+
|
|
176
|
+
# Remove metadata file
|
|
177
|
+
if metadata_path.exists():
|
|
178
|
+
metadata_path.unlink()
|
|
179
|
+
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.error(f"🚨 [APP] Error cleaning up sent frame: {e}")
|
|
182
|
+
|
|
183
|
+
def get_pending_frame_count(self) -> int:
|
|
184
|
+
"""
|
|
185
|
+
Get the total number of pending frames to be sent.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
int: Number of pending frames
|
|
189
|
+
"""
|
|
190
|
+
pending_count = 0
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
if not self.dataset_frames_path.exists():
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
for dataset_source_dir in self.dataset_frames_path.iterdir():
|
|
197
|
+
if not dataset_source_dir.is_dir():
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
for file_path in dataset_source_dir.glob("*.jpg"):
|
|
201
|
+
frame_uuid = file_path.stem.split('_')[0]
|
|
202
|
+
if frame_uuid not in self.sent_frames:
|
|
203
|
+
pending_count += 1
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
logger.error(f"🚨 [APP] Error counting pending frames: {e}")
|
|
207
|
+
|
|
208
|
+
return pending_count
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
import logging
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Dict
|
|
7
|
+
from ..services.WorkerSourcePipelineClient import WorkerSourcePipelineClient
|
|
8
|
+
from ..services.GrpcClientManager import GrpcClientManager
|
|
9
|
+
from ..repositories.DatasetSourceRepository import DatasetSourceRepository
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
def safe_join_thread(thread, timeout=5):
|
|
14
|
+
"""Safely join a thread, avoiding RuntimeError when joining current thread."""
|
|
15
|
+
if thread and thread != threading.current_thread():
|
|
16
|
+
thread.join(timeout=timeout)
|
|
17
|
+
elif thread == threading.current_thread():
|
|
18
|
+
logging.info("🛑 [APP] Thread stopping from within itself, skipping join.")
|
|
19
|
+
|
|
20
|
+
class DatasetSourceThread:
|
|
21
|
+
"""Individual thread for handling a single dataset source."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, dataset_source, pipeline_client, storage_path):
|
|
24
|
+
self.dataset_source = dataset_source
|
|
25
|
+
self.pipeline_client = pipeline_client
|
|
26
|
+
self.storage_path = storage_path
|
|
27
|
+
self.thread = None
|
|
28
|
+
self.stop_event = threading.Event()
|
|
29
|
+
self.last_frame_time = 0
|
|
30
|
+
self.lock = threading.Lock()
|
|
31
|
+
|
|
32
|
+
# Create storage directory for this dataset source
|
|
33
|
+
self.dataset_storage_path = storage_path / "dataset_frames" / dataset_source.id
|
|
34
|
+
self.dataset_storage_path.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
# Track consecutive failures
|
|
37
|
+
self.consecutive_failures = 0
|
|
38
|
+
self.max_consecutive_failures = 5
|
|
39
|
+
|
|
40
|
+
def start(self):
|
|
41
|
+
"""Start the dataset source thread."""
|
|
42
|
+
if self.thread and self.thread.is_alive():
|
|
43
|
+
logger.warning(f"⚠️ [APP] Thread for dataset source {self.dataset_source.id} is already running.")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
self.stop_event.clear()
|
|
47
|
+
self.consecutive_failures = 0 # Reset failure counter
|
|
48
|
+
self.thread = threading.Thread(
|
|
49
|
+
target=self._run,
|
|
50
|
+
daemon=True,
|
|
51
|
+
name=f"DatasetSource-{self.dataset_source.id}"
|
|
52
|
+
)
|
|
53
|
+
self.thread.start()
|
|
54
|
+
logger.info(f"🚀 [APP] Started thread for dataset source {self.dataset_source.id} ({self.dataset_source.dataset_name})")
|
|
55
|
+
|
|
56
|
+
def stop(self):
|
|
57
|
+
"""Stop the dataset source thread."""
|
|
58
|
+
if not self.thread or not self.thread.is_alive():
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
self.stop_event.set()
|
|
62
|
+
safe_join_thread(self.thread)
|
|
63
|
+
logger.info(f"🛑 [APP] Stopped thread for dataset source {self.dataset_source.id}")
|
|
64
|
+
|
|
65
|
+
def _run(self):
|
|
66
|
+
"""Main loop for this dataset source."""
|
|
67
|
+
try:
|
|
68
|
+
while not self.stop_event.is_set():
|
|
69
|
+
current_time = time.time()
|
|
70
|
+
|
|
71
|
+
# Check if it's time to capture a frame
|
|
72
|
+
if (current_time - self.last_frame_time) >= self.dataset_source.sampling_interval:
|
|
73
|
+
success = self._capture_frame()
|
|
74
|
+
|
|
75
|
+
with self.lock:
|
|
76
|
+
self.last_frame_time = current_time
|
|
77
|
+
if success:
|
|
78
|
+
self.consecutive_failures = 0 # Reset on success
|
|
79
|
+
else:
|
|
80
|
+
self.consecutive_failures += 1
|
|
81
|
+
|
|
82
|
+
# If too many consecutive failures, log warning and pause
|
|
83
|
+
if self.consecutive_failures >= self.max_consecutive_failures:
|
|
84
|
+
logger.warning(f"⚠️ [APP] Dataset source {self.dataset_source.id} has {self.consecutive_failures} consecutive failures. Pausing for 30 seconds.")
|
|
85
|
+
time.sleep(30) # Pause for 30 seconds
|
|
86
|
+
self.consecutive_failures = 0 # Reset after pause
|
|
87
|
+
|
|
88
|
+
# Sleep for a shorter interval to be more responsive
|
|
89
|
+
time.sleep(min(1, self.dataset_source.sampling_interval / 10))
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error(f"🚨 [APP] Error in dataset source thread {self.dataset_source.id}: {e}", exc_info=True)
|
|
93
|
+
|
|
94
|
+
def _capture_frame(self):
|
|
95
|
+
"""Capture and save a frame for this dataset source. Returns True if successful."""
|
|
96
|
+
try:
|
|
97
|
+
# Get frame from source
|
|
98
|
+
frame_bytes = self._get_frame_from_source(self.dataset_source.worker_source_url)
|
|
99
|
+
|
|
100
|
+
if frame_bytes:
|
|
101
|
+
# Generate unique filename
|
|
102
|
+
timestamp = int(time.time() * 1000)
|
|
103
|
+
frame_uuid = str(uuid.uuid4())
|
|
104
|
+
filename = f"{frame_uuid}_{timestamp}.jpg"
|
|
105
|
+
file_path = self.dataset_storage_path / filename
|
|
106
|
+
|
|
107
|
+
# Save frame to local storage
|
|
108
|
+
with open(file_path, 'wb') as f:
|
|
109
|
+
f.write(frame_bytes)
|
|
110
|
+
|
|
111
|
+
# Create metadata file
|
|
112
|
+
metadata = {
|
|
113
|
+
"dataset_source_id": self.dataset_source.id,
|
|
114
|
+
"dataset_id": self.dataset_source.dataset_id,
|
|
115
|
+
"worker_source_id": self.dataset_source.worker_source_id,
|
|
116
|
+
"dataset_name": self.dataset_source.dataset_name,
|
|
117
|
+
"worker_source_name": self.dataset_source.worker_source_name,
|
|
118
|
+
"worker_source_url": self.dataset_source.worker_source_url,
|
|
119
|
+
"frame_uuid": frame_uuid,
|
|
120
|
+
"timestamp": timestamp,
|
|
121
|
+
"captured_at": datetime.utcnow().isoformat()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
metadata_path = file_path.with_suffix('.json')
|
|
125
|
+
import json
|
|
126
|
+
with open(metadata_path, 'w') as f:
|
|
127
|
+
json.dump(metadata, f, indent=2)
|
|
128
|
+
|
|
129
|
+
logger.info(f"📸 [APP] Captured frame for {self.dataset_source.dataset_name} (ID: {self.dataset_source.id})")
|
|
130
|
+
return True
|
|
131
|
+
else:
|
|
132
|
+
logger.warning(f"⚠️ [APP] Could not get frame from source {self.dataset_source.worker_source_url}")
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.error(f"🚨 [APP] Error capturing frame for {self.dataset_source.dataset_name}: {e}", exc_info=True)
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
def _get_frame_from_source(self, source_url):
|
|
140
|
+
"""Get a frame from the given source URL."""
|
|
141
|
+
try:
|
|
142
|
+
stream_type = self.pipeline_client._detect_stream_type(source_url)
|
|
143
|
+
if stream_type == "video_file":
|
|
144
|
+
logger.info(f"🎬 [APP] Capturing video frame from {source_url}")
|
|
145
|
+
elif stream_type == "image_file":
|
|
146
|
+
logger.info(f"🖼️ [APP] Capturing image frame from {source_url}")
|
|
147
|
+
elif stream_type in ["rtsp", "hls"]:
|
|
148
|
+
logger.info(f"📡 [APP] Capturing live stream frame from {source_url}")
|
|
149
|
+
|
|
150
|
+
frame_bytes = self.pipeline_client._get_single_frame_bytes(source_url)
|
|
151
|
+
|
|
152
|
+
if frame_bytes and stream_type == "video_file":
|
|
153
|
+
status = self.pipeline_client.get_video_positions_status()
|
|
154
|
+
for video_path, info in status.items():
|
|
155
|
+
if info["duration"]:
|
|
156
|
+
logger.info(f"📊 [APP] Video progress: {info['progress_percent']:.1f}% ({info['current_position']:.2f}s / {info['duration']:.2f}s)")
|
|
157
|
+
|
|
158
|
+
return frame_bytes
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(f"🚨 [APP] Error getting frame from source {source_url}: {e}", exc_info=True)
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
class DatasetFrameWorker:
|
|
164
|
+
def __init__(self, config: dict):
|
|
165
|
+
"""
|
|
166
|
+
Initialize Dataset Frame Worker.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
config (dict): Configuration object containing settings.
|
|
170
|
+
"""
|
|
171
|
+
if not isinstance(config, dict):
|
|
172
|
+
raise ValueError("⚠️ [APP] config must be a dictionary.")
|
|
173
|
+
|
|
174
|
+
self.config = config
|
|
175
|
+
self.worker_id = self.config.get("worker_id")
|
|
176
|
+
self.server_host = self.config.get("server_host")
|
|
177
|
+
self.token = self.config.get("token")
|
|
178
|
+
|
|
179
|
+
if not self.worker_id:
|
|
180
|
+
raise ValueError("⚠️ [APP] Configuration is missing 'worker_id'.")
|
|
181
|
+
if not self.token:
|
|
182
|
+
raise ValueError("⚠️ [APP] Configuration is missing 'token'.")
|
|
183
|
+
|
|
184
|
+
self.dataset_source_repo = DatasetSourceRepository()
|
|
185
|
+
|
|
186
|
+
# Get shared client instance from the centralized manager
|
|
187
|
+
self.client_manager = GrpcClientManager.get_instance()
|
|
188
|
+
self.worker_source_pipeline_client = self.client_manager.get_client(WorkerSourcePipelineClient)
|
|
189
|
+
|
|
190
|
+
self.thread = None
|
|
191
|
+
self.stop_event = threading.Event()
|
|
192
|
+
self.lock = threading.Lock()
|
|
193
|
+
|
|
194
|
+
# Cache for dataset source threads
|
|
195
|
+
self.dataset_source_threads: Dict[str, DatasetSourceThread] = {}
|
|
196
|
+
self.last_sync_time = 0
|
|
197
|
+
self.sync_interval = 30 # Sync dataset sources every 30 seconds
|
|
198
|
+
|
|
199
|
+
# Thread for syncing dataset sources
|
|
200
|
+
self.sync_thread = None
|
|
201
|
+
|
|
202
|
+
# Sync lock to prevent multiple simultaneous sync operations
|
|
203
|
+
self.sync_lock = threading.Lock()
|
|
204
|
+
|
|
205
|
+
# Storage path for dataset frames
|
|
206
|
+
from ..database.DatabaseManager import get_storage_path
|
|
207
|
+
self.storage_path = get_storage_path("files")
|
|
208
|
+
|
|
209
|
+
def start(self):
|
|
210
|
+
"""Start the Dataset Frame Worker."""
|
|
211
|
+
with self.lock:
|
|
212
|
+
if self.thread and self.thread.is_alive():
|
|
213
|
+
logger.warning("⚠️ [APP] Dataset Frame Worker is already running.")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
self.stop_event.clear()
|
|
217
|
+
|
|
218
|
+
# Start sync thread
|
|
219
|
+
self.sync_thread = threading.Thread(target=self._sync_loop, daemon=True)
|
|
220
|
+
self.sync_thread.start()
|
|
221
|
+
|
|
222
|
+
# Start main worker thread
|
|
223
|
+
self.thread = threading.Thread(target=self._run, daemon=True)
|
|
224
|
+
self.thread.start()
|
|
225
|
+
logger.info(f"🚀 [APP] Dataset Frame Worker started (Device: {self.worker_id}).")
|
|
226
|
+
|
|
227
|
+
def stop(self):
|
|
228
|
+
"""Stop the Dataset Frame Worker."""
|
|
229
|
+
with self.lock:
|
|
230
|
+
if not self.thread or not self.thread.is_alive():
|
|
231
|
+
logger.warning("⚠️ [APP] Dataset Frame Worker is not running.")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
self.stop_event.set()
|
|
235
|
+
|
|
236
|
+
# Stop all dataset source threads
|
|
237
|
+
for thread in self.dataset_source_threads.values():
|
|
238
|
+
thread.stop()
|
|
239
|
+
|
|
240
|
+
# Wait for threads to stop
|
|
241
|
+
if self.thread:
|
|
242
|
+
safe_join_thread(self.thread)
|
|
243
|
+
if self.sync_thread:
|
|
244
|
+
safe_join_thread(self.sync_thread)
|
|
245
|
+
|
|
246
|
+
self.thread = None
|
|
247
|
+
self.sync_thread = None
|
|
248
|
+
logger.info(f"🛑 [APP] Dataset Frame Worker stopped (Device: {self.worker_id}).")
|
|
249
|
+
|
|
250
|
+
def _run(self):
|
|
251
|
+
"""Main loop for managing dataset source threads."""
|
|
252
|
+
try:
|
|
253
|
+
while not self.stop_event.is_set():
|
|
254
|
+
self._manage_dataset_source_threads()
|
|
255
|
+
time.sleep(5) # Check every 5 seconds
|
|
256
|
+
except Exception as e:
|
|
257
|
+
logger.error("🚨 [APP] Unexpected error in Dataset Frame Worker main loop.", exc_info=True)
|
|
258
|
+
|
|
259
|
+
def _sync_loop(self):
|
|
260
|
+
"""Background thread for syncing dataset sources."""
|
|
261
|
+
try:
|
|
262
|
+
while not self.stop_event.is_set():
|
|
263
|
+
self._sync_dataset_sources()
|
|
264
|
+
time.sleep(self.sync_interval)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.error("🚨 [APP] Error in dataset source sync loop.", exc_info=True)
|
|
267
|
+
|
|
268
|
+
def _sync_dataset_sources(self):
|
|
269
|
+
"""Sync dataset sources from server."""
|
|
270
|
+
# Prevent multiple simultaneous sync operations
|
|
271
|
+
if not self.sync_lock.acquire(blocking=False):
|
|
272
|
+
logger.debug("🔄 [APP] Sync operation already in progress, skipping...")
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
from ..services.DatasetSourceClient import DatasetSourceClient
|
|
277
|
+
# Use shared client instead of creating new instance
|
|
278
|
+
client = self.client_manager.get_client(DatasetSourceClient, "DatasetSourceClient")
|
|
279
|
+
response = client.get_dataset_source_list(self.token)
|
|
280
|
+
|
|
281
|
+
if response and response.get("success"):
|
|
282
|
+
dataset_sources_data = response.get("data", [])
|
|
283
|
+
self.dataset_source_repo.sync_dataset_sources(dataset_sources_data)
|
|
284
|
+
self.last_sync_time = time.time()
|
|
285
|
+
else:
|
|
286
|
+
error_message = response.get("message", "Unknown error") if response else "Unknown error"
|
|
287
|
+
logger.error(f"❌ [APP] Failed to sync dataset sources: {error_message}")
|
|
288
|
+
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.error("🚨 [APP] Error syncing dataset sources.", exc_info=True)
|
|
291
|
+
finally:
|
|
292
|
+
self.sync_lock.release()
|
|
293
|
+
|
|
294
|
+
def _cleanup_orphaned_frames(self, deleted_dataset_source_ids):
|
|
295
|
+
"""Clean up frames for deleted dataset sources."""
|
|
296
|
+
try:
|
|
297
|
+
for dataset_source_id in deleted_dataset_source_ids:
|
|
298
|
+
orphaned_frames_path = self.storage_path / "dataset_frames" / dataset_source_id
|
|
299
|
+
if orphaned_frames_path.exists():
|
|
300
|
+
import shutil
|
|
301
|
+
shutil.rmtree(orphaned_frames_path)
|
|
302
|
+
logger.info(f"🗑️ [APP] Cleaned up orphaned frames for deleted dataset source {dataset_source_id}")
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.error(f"🚨 [APP] Error cleaning up orphaned frames: {e}", exc_info=True)
|
|
305
|
+
|
|
306
|
+
def _manage_dataset_source_threads(self):
|
|
307
|
+
"""Manage dataset source threads based on current dataset sources."""
|
|
308
|
+
try:
|
|
309
|
+
# Get current dataset sources from local database
|
|
310
|
+
dataset_sources = self.dataset_source_repo.get_all_dataset_sources()
|
|
311
|
+
current_dataset_source_ids = {ds.id for ds in dataset_sources}
|
|
312
|
+
|
|
313
|
+
# Stop threads for dataset sources that no longer exist
|
|
314
|
+
threads_to_remove = []
|
|
315
|
+
deleted_dataset_source_ids = []
|
|
316
|
+
for dataset_source_id, thread in self.dataset_source_threads.items():
|
|
317
|
+
if dataset_source_id not in current_dataset_source_ids:
|
|
318
|
+
logger.info(f"🛑 [APP] Stopping thread for deleted dataset source {dataset_source_id}")
|
|
319
|
+
thread.stop()
|
|
320
|
+
threads_to_remove.append(dataset_source_id)
|
|
321
|
+
deleted_dataset_source_ids.append(dataset_source_id)
|
|
322
|
+
|
|
323
|
+
for dataset_source_id in threads_to_remove:
|
|
324
|
+
del self.dataset_source_threads[dataset_source_id]
|
|
325
|
+
|
|
326
|
+
# Clean up orphaned frames for deleted dataset sources
|
|
327
|
+
if deleted_dataset_source_ids:
|
|
328
|
+
self._cleanup_orphaned_frames(deleted_dataset_source_ids)
|
|
329
|
+
|
|
330
|
+
# Process current dataset sources
|
|
331
|
+
for dataset_source in dataset_sources:
|
|
332
|
+
if dataset_source.id not in self.dataset_source_threads:
|
|
333
|
+
# Create new thread for new dataset source
|
|
334
|
+
logger.info(f"🆕 [APP] Creating new thread for dataset source {dataset_source.id} ({dataset_source.dataset_name})")
|
|
335
|
+
thread = DatasetSourceThread(
|
|
336
|
+
dataset_source=dataset_source,
|
|
337
|
+
pipeline_client=self.worker_source_pipeline_client,
|
|
338
|
+
storage_path=self.storage_path
|
|
339
|
+
)
|
|
340
|
+
self.dataset_source_threads[dataset_source.id] = thread
|
|
341
|
+
thread.start()
|
|
342
|
+
else:
|
|
343
|
+
# Update existing thread with new dataset source data
|
|
344
|
+
existing_thread = self.dataset_source_threads[dataset_source.id]
|
|
345
|
+
if self._dataset_source_changed(existing_thread.dataset_source, dataset_source):
|
|
346
|
+
logger.info(f"🔄 [APP] Updating thread for dataset source {dataset_source.id} ({dataset_source.dataset_name})")
|
|
347
|
+
# Stop the old thread
|
|
348
|
+
existing_thread.stop()
|
|
349
|
+
# Create new thread with updated data
|
|
350
|
+
new_thread = DatasetSourceThread(
|
|
351
|
+
dataset_source=dataset_source,
|
|
352
|
+
pipeline_client=self.worker_source_pipeline_client,
|
|
353
|
+
storage_path=self.storage_path
|
|
354
|
+
)
|
|
355
|
+
self.dataset_source_threads[dataset_source.id] = new_thread
|
|
356
|
+
new_thread.start()
|
|
357
|
+
|
|
358
|
+
# Log current status
|
|
359
|
+
active_threads = len([t for t in self.dataset_source_threads.values() if t.thread and t.thread.is_alive()])
|
|
360
|
+
logger.debug(f"📊 [APP] Dataset Frame Worker status: {active_threads} active threads, {len(dataset_sources)} total dataset sources")
|
|
361
|
+
|
|
362
|
+
except Exception as e:
|
|
363
|
+
logger.error("🚨 [APP] Error managing dataset source threads.", exc_info=True)
|
|
364
|
+
|
|
365
|
+
def _dataset_source_changed(self, old_dataset_source, new_dataset_source):
|
|
366
|
+
"""Check if dataset source data has changed significantly."""
|
|
367
|
+
try:
|
|
368
|
+
# Compare relevant fields that would affect thread behavior
|
|
369
|
+
fields_to_compare = [
|
|
370
|
+
'worker_source_url',
|
|
371
|
+
'sampling_interval',
|
|
372
|
+
'dataset_name',
|
|
373
|
+
'worker_source_name',
|
|
374
|
+
'dataset_id',
|
|
375
|
+
'worker_source_id'
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
for field in fields_to_compare:
|
|
379
|
+
old_value = getattr(old_dataset_source, field, None)
|
|
380
|
+
new_value = getattr(new_dataset_source, field, None)
|
|
381
|
+
if old_value != new_value:
|
|
382
|
+
logger.debug(f"🔄 [APP] Dataset source {new_dataset_source.id} field '{field}' changed: {old_value} -> {new_value}")
|
|
383
|
+
return True
|
|
384
|
+
|
|
385
|
+
return False
|
|
386
|
+
|
|
387
|
+
except Exception as e:
|
|
388
|
+
logger.error(f"🚨 [APP] Error comparing dataset sources: {e}", exc_info=True)
|
|
389
|
+
return True # Assume changed if comparison fails
|
|
390
|
+
|
|
391
|
+
def get_status(self):
|
|
392
|
+
"""Get current status of dataset frame worker."""
|
|
393
|
+
try:
|
|
394
|
+
dataset_sources = self.dataset_source_repo.get_all_dataset_sources()
|
|
395
|
+
active_threads = [t for t in self.dataset_source_threads.values() if t.thread and t.thread.is_alive()]
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
"total_dataset_sources": len(dataset_sources),
|
|
399
|
+
"active_threads": len(active_threads),
|
|
400
|
+
"thread_details": [
|
|
401
|
+
{
|
|
402
|
+
"dataset_source_id": t.dataset_source.id,
|
|
403
|
+
"dataset_name": t.dataset_source.dataset_name,
|
|
404
|
+
"is_alive": t.thread.is_alive() if t.thread else False,
|
|
405
|
+
"consecutive_failures": t.consecutive_failures
|
|
406
|
+
}
|
|
407
|
+
for t in self.dataset_source_threads.values()
|
|
408
|
+
]
|
|
409
|
+
}
|
|
410
|
+
except Exception as e:
|
|
411
|
+
logger.error(f"🚨 [APP] Error getting dataset frame worker status: {e}", exc_info=True)
|
|
412
|
+
return {"error": str(e)}
|